Merge "Make communal_hub flag dependent on keyguard_shade_migration_nssl" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 1c6df75..79b4fd6 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -855,6 +855,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "device_policy_aconfig_flags_lib_host",
+    aconfig_declarations: "device_policy_aconfig_flags",
+    host_supported: true,
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 cc_aconfig_library {
     name: "device_policy_aconfig_flags_c_lib",
     aconfig_declarations: "device_policy_aconfig_flags",
diff --git a/Ravenwood.bp b/Ravenwood.bp
index d13c4d7..93febca4 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -33,6 +33,7 @@
         "@$(location ravenwood/ravenwood-standard-options.txt) " +
 
         "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
+        "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
 
         "--out-impl-jar $(location ravenwood.jar) " +
 
@@ -56,6 +57,7 @@
         "hoststubgen_dump.txt",
 
         "hoststubgen_framework-minus-apex.log",
+        "hoststubgen_framework-minus-apex_stats.csv",
     ],
     visibility: ["//visibility:private"],
 }
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 7284f47..7de6799 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -2354,9 +2354,9 @@
                 if (maxExecutionDelayMillis - windowStart < MIN_ALLOWED_TIME_WINDOW_MILLIS) {
                     if (enforceMinimumTimeWindows
                             && Flags.enforceMinimumTimeWindows()) {
-                        throw new IllegalArgumentException("Jobs with a deadline and"
-                                + " functional constraints cannot have a time window less than "
-                                + MIN_ALLOWED_TIME_WINDOW_MILLIS + " ms."
+                        throw new IllegalArgumentException("Time window too short. Constraints"
+                                + " unlikely to be satisfied. Increase deadline to a reasonable"
+                                + " duration."
                                 + " Job '" + service.flattenToShortString() + "#" + jobId + "'"
                                 + " has delay=" + windowStart
                                 + ", deadline=" + maxExecutionDelayMillis);
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 6883d18..aec464d 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
@@ -241,6 +241,8 @@
         private static final long MAX_TIME_WINDOW_MS = 24 * HOUR_IN_MILLIS;
         private final JobScoreBucket[] mScoreBuckets = new JobScoreBucket[NUM_SCORE_BUCKETS];
         private int mScoreBucketIndex = 0;
+        private long mCachedScoreExpirationTimeElapsed;
+        private int mCachedScore;
 
         public void addScore(int add, long nowElapsed) {
             JobScoreBucket bucket = mScoreBuckets[mScoreBucketIndex];
@@ -248,10 +250,17 @@
                 bucket = new JobScoreBucket();
                 bucket.startTimeElapsed = nowElapsed;
                 mScoreBuckets[mScoreBucketIndex] = bucket;
+                // Brand new bucket, there's nothing to remove from the score,
+                // so just update the expiration time if needed.
+                mCachedScoreExpirationTimeElapsed = Math.min(mCachedScoreExpirationTimeElapsed,
+                        nowElapsed + MAX_TIME_WINDOW_MS);
             } else if (bucket.startTimeElapsed < nowElapsed - MAX_TIME_WINDOW_MS) {
                 // The bucket is too old.
                 bucket.reset();
                 bucket.startTimeElapsed = nowElapsed;
+                // Force a recalculation of the cached score instead of just updating the cached
+                // value and time in case there are multiple stale buckets.
+                mCachedScoreExpirationTimeElapsed = nowElapsed;
             } else if (bucket.startTimeElapsed
                     < nowElapsed - MAX_TIME_WINDOW_MS / NUM_SCORE_BUCKETS) {
                 // The current bucket's duration has completed. Move on to the next bucket.
@@ -261,16 +270,26 @@
             }
 
             bucket.score += add;
+            mCachedScore += add;
         }
 
         public int getScore(long nowElapsed) {
+            if (nowElapsed < mCachedScoreExpirationTimeElapsed) {
+                return mCachedScore;
+            }
             int score = 0;
             final long earliestElapsed = nowElapsed - MAX_TIME_WINDOW_MS;
+            long earliestValidBucketTimeElapsed = Long.MAX_VALUE;
             for (JobScoreBucket bucket : mScoreBuckets) {
                 if (bucket != null && bucket.startTimeElapsed >= earliestElapsed) {
                     score += bucket.score;
+                    if (earliestValidBucketTimeElapsed > bucket.startTimeElapsed) {
+                        earliestValidBucketTimeElapsed = bucket.startTimeElapsed;
+                    }
                 }
             }
+            mCachedScore = score;
+            mCachedScoreExpirationTimeElapsed = earliestValidBucketTimeElapsed + MAX_TIME_WINDOW_MS;
             return score;
         }
 
@@ -378,10 +397,16 @@
 
     @Override
     public void prepareForExecutionLocked(JobStatus jobStatus) {
+        if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
+            // Don't include jobs for the TOP app in the score calculation.
+            return;
+        }
         // Use the job's requested priority to determine its score since that is what the developer
         // selected and it will be stable across job runs.
-        final int score = mFallbackFlexibilityDeadlineScores
-                .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100);
+        final int priority = jobStatus.getJob().getPriority();
+        final int score = mFallbackFlexibilityDeadlineScores.get(priority,
+                FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES
+                        .get(priority, priority / 100));
         JobScoreTracker jobScoreTracker =
                 mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName());
         if (jobScoreTracker == null) {
@@ -394,6 +419,10 @@
 
     @Override
     public void unprepareFromExecutionLocked(JobStatus jobStatus) {
+        if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
+            // Jobs for the TOP app are excluded from the score calculation.
+            return;
+        }
         // The job didn't actually start. Undo the score increase.
         JobScoreTracker jobScoreTracker =
                 mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName());
@@ -401,8 +430,10 @@
             Slog.e(TAG, "Unprepared a job that didn't result in a score change");
             return;
         }
-        final int score = mFallbackFlexibilityDeadlineScores
-                .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100);
+        final int priority = jobStatus.getJob().getPriority();
+        final int score = mFallbackFlexibilityDeadlineScores.get(priority,
+                FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES
+                        .get(priority, priority / 100));
         jobScoreTracker.addScore(-score, sElapsedRealtimeClock.millis());
     }
 
@@ -649,21 +680,24 @@
                     (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2),
                     mMaxRescheduledDeadline);
         }
+
+        // Intentionally use the effective priority here. If a job's priority was effectively
+        // lowered, it will be less likely to run quickly given other policies in JobScheduler.
+        // Thus, there's no need to further delay the job based on flex policy.
+        final int jobPriority = js.getEffectivePriority();
+        final int jobScore =
+                getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed);
+        // Set an upper limit on the fallback deadline so that the delay doesn't become extreme.
+        final long fallbackDurationMs = Math.min(3 * mFallbackFlexibilityDeadlineMs,
+                mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs)
+                        + mFallbackFlexibilityAdditionalScoreTimeFactors
+                                .get(jobPriority, MINUTE_IN_MILLIS) * jobScore);
+        final long fallbackDeadlineMs = earliest + fallbackDurationMs;
+
         if (js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME) {
-            // Intentionally use the effective priority here. If a job's priority was effectively
-            // lowered, it will be less likely to run quickly given other policies in JobScheduler.
-            // Thus, there's no need to further delay the job based on flex policy.
-            final int jobPriority = js.getEffectivePriority();
-            final int jobScore =
-                    getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed);
-            // Set an upper limit on the fallback deadline so that the delay doesn't become extreme.
-            final long fallbackDeadlineMs = Math.min(3 * mFallbackFlexibilityDeadlineMs,
-                    mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs)
-                            + mFallbackFlexibilityAdditionalScoreTimeFactors
-                                    .get(jobPriority, MINUTE_IN_MILLIS) * jobScore);
-            return earliest + fallbackDeadlineMs;
+            return fallbackDeadlineMs;
         }
-        return js.getLatestRunTimeElapsed();
+        return Math.max(fallbackDeadlineMs, js.getLatestRunTimeElapsed());
     }
 
     @VisibleForTesting
@@ -976,7 +1010,8 @@
                     // Something has gone horribly wrong. This has only occurred on incorrectly
                     // configured tests, but add a check here for safety.
                     Slog.wtf(TAG, "Got invalid latest when scheduling alarm."
-                            + " Prefetch=" + js.getJob().isPrefetch());
+                            + " prefetch=" + js.getJob().isPrefetch()
+                            + " periodic=" + js.getJob().isPeriodic());
                     // Since things have gone wrong, the safest and most reliable thing to do is
                     // stop applying flex policy to the job.
                     mFlexibilityTracker.setNumDroppedFlexibleConstraints(js,
@@ -991,7 +1026,7 @@
 
                 if (DEBUG) {
                     Slog.d(TAG, "scheduleDropNumConstraintsAlarm: "
-                            + js.getSourcePackageName() + " " + js.getSourceUserId()
+                            + js.toShortString()
                             + " numApplied: " + js.getNumAppliedFlexibleConstraints()
                             + " numRequired: " + js.getNumRequiredFlexibleConstraints()
                             + " numSatisfied: " + Integer.bitCount(
@@ -1199,11 +1234,11 @@
             DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
                     .put(PRIORITY_MAX, 0);
             DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
-                    .put(PRIORITY_HIGH, 4 * MINUTE_IN_MILLIS);
+                    .put(PRIORITY_HIGH, 3 * MINUTE_IN_MILLIS);
             DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
-                    .put(PRIORITY_DEFAULT, 3 * MINUTE_IN_MILLIS);
+                    .put(PRIORITY_DEFAULT, 2 * MINUTE_IN_MILLIS);
             DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
-                    .put(PRIORITY_LOW, 2 * MINUTE_IN_MILLIS);
+                    .put(PRIORITY_LOW, 1 * MINUTE_IN_MILLIS);
             DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
                     .put(PRIORITY_MIN, 1 * MINUTE_IN_MILLIS);
             DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
@@ -1220,7 +1255,7 @@
 
         private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS;
         private static final long DEFAULT_RESCHEDULED_JOB_DEADLINE_MS = HOUR_IN_MILLIS;
-        private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = 5 * DAY_IN_MILLIS;
+        private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = DAY_IN_MILLIS;
         @VisibleForTesting
         static final long DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = 3 * DAY_IN_MILLIS;
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index a3a686f..a0b9c5f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -16,8 +16,6 @@
 
 package com.android.server.job.controllers;
 
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-
 import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
 import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
 import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
@@ -430,9 +428,6 @@
      */
     public static final int INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ = 1 << 2;
 
-    /** Minimum difference between start and end time to have flexible constraint */
-    @VisibleForTesting
-    static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS;
     /**
      * Versatile, persistable flags for a job that's updated within the system server,
      * as opposed to {@link JobInfo#flags} that's set by callers.
@@ -708,14 +703,10 @@
         final boolean lacksSomeFlexibleConstraints =
                 ((~requiredConstraints) & SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS) != 0
                         || mCanApplyTransportAffinities;
-        final boolean satisfiesMinWindowException =
-                (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis)
-                >= MIN_WINDOW_FOR_FLEXIBILITY_MS;
 
         // The first time a job is rescheduled it will not be subject to flexible constraints.
         // Otherwise, every consecutive reschedule increases a jobs' flexibility deadline.
         if (!isRequestedExpeditedJob() && !job.isUserInitiated()
-                && satisfiesMinWindowException
                 && (numFailures + numSystemStops) != 1
                 && lacksSomeFlexibleConstraints) {
             requiredConstraints |= CONSTRAINT_FLEXIBLE;
diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md
index f177586..b6e4e0d 100644
--- a/cmds/uinput/README.md
+++ b/cmds/uinput/README.md
@@ -154,8 +154,7 @@
 
 #### `delay`
 
-Add a delay between the processing of commands. The delay will be timed from when the last delay
-ended, rather than from the current time, to allow for more precise timings to be produced.
+Add a delay to command processing
 
 | Field         | Type          | Description                |
 |:-------------:|:-------------:|:-------------------------- |
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
index bd61000..a78a465 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
@@ -166,14 +166,14 @@
     ::ioctl(mFd, UI_DEV_DESTROY);
 }
 
-void UinputDevice::injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code,
-                               int32_t value) {
+void UinputDevice::injectEvent(uint16_t type, uint16_t code, int32_t value) {
     struct input_event event = {};
     event.type = type;
     event.code = code;
     event.value = value;
-    event.time.tv_sec = timestamp.count() / 1'000'000;
-    event.time.tv_usec = timestamp.count() % 1'000'000;
+    timespec ts;
+    clock_gettime(CLOCK_MONOTONIC, &ts);
+    TIMESPEC_TO_TIMEVAL(&event.time, &ts);
 
     if (::write(mFd, &event, sizeof(input_event)) < 0) {
         ALOGE("Could not write event %" PRIu16 " %" PRIu16 " with value %" PRId32 " : %s", type,
@@ -268,12 +268,12 @@
     }
 }
 
-static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jlong timestampMicros,
-                        jint type, jint code, jint value) {
+static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint type, jint code,
+                        jint value) {
     uinput::UinputDevice* d = reinterpret_cast<uinput::UinputDevice*>(ptr);
     if (d != nullptr) {
-        d->injectEvent(std::chrono::microseconds(timestampMicros), static_cast<uint16_t>(type),
-                       static_cast<uint16_t>(code), static_cast<int32_t>(value));
+        d->injectEvent(static_cast<uint16_t>(type), static_cast<uint16_t>(code),
+                       static_cast<int32_t>(value));
     } else {
         ALOGE("Could not inject event, Device* is null!");
     }
@@ -330,7 +330,7 @@
          "(Ljava/lang/String;IIIIIILjava/lang/String;"
          "Lcom/android/commands/uinput/Device$DeviceCallback;)J",
          reinterpret_cast<void*>(openUinputDevice)},
-        {"nativeInjectEvent", "(JJIII)V", reinterpret_cast<void*>(injectEvent)},
+        {"nativeInjectEvent", "(JIII)V", reinterpret_cast<void*>(injectEvent)},
         {"nativeConfigure", "(II[I)V", reinterpret_cast<void*>(configure)},
         {"nativeSetAbsInfo", "(IILandroid/os/Parcel;)V", reinterpret_cast<void*>(setAbsInfo)},
         {"nativeCloseUinputDevice", "(J)V", reinterpret_cast<void*>(closeUinputDevice)},
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.h b/cmds/uinput/jni/com_android_commands_uinput_Device.h
index 72c8647..9769a75 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.h
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.h
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
-#include <android-base/unique_fd.h>
-#include <jni.h>
-#include <linux/input.h>
-
-#include <chrono>
 #include <memory>
 #include <vector>
 
+#include <jni.h>
+#include <linux/input.h>
+
+#include <android-base/unique_fd.h>
 #include "src/com/android/commands/uinput/InputAbsInfo.h"
 
 namespace android {
@@ -54,8 +53,7 @@
 
     virtual ~UinputDevice();
 
-    void injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code,
-                     int32_t value);
+    void injectEvent(uint16_t type, uint16_t code, int32_t value);
     int handleEvents(int events);
 
 private:
diff --git a/cmds/uinput/src/com/android/commands/uinput/Device.java b/cmds/uinput/src/com/android/commands/uinput/Device.java
index 76ab475..25d3a34 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Device.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Device.java
@@ -55,7 +55,7 @@
     private final SparseArray<InputAbsInfo> mAbsInfo;
     private final OutputStream mOutputStream;
     private final Object mCond = new Object();
-    private long mTimeToSendNanos;
+    private long mTimeToSend;
 
     static {
         System.loadLibrary("uinputcommand_jni");
@@ -65,8 +65,7 @@
             int productId, int versionId, int bus, int ffEffectsMax, String port,
             DeviceCallback callback);
     private static native void nativeCloseUinputDevice(long ptr);
-    private static native void nativeInjectEvent(long ptr, long timestampMicros, int type, int code,
-                                                 int value);
+    private static native void nativeInjectEvent(long ptr, int type, int code, int value);
     private static native void nativeConfigure(int handle, int code, int[] configs);
     private static native void nativeSetAbsInfo(int handle, int axisCode, Parcel axisParcel);
     private static native int nativeGetEvdevEventTypeByLabel(String label);
@@ -102,54 +101,27 @@
         }
 
         mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget();
-        mTimeToSendNanos = SystemClock.uptimeNanos();
-    }
-
-    private long getTimeToSendMillis() {
-        // Since we can only specify delays in milliseconds but evemu timestamps are in
-        // microseconds, we have to round up the delays to avoid setting event timestamps
-        // which are in the future (which the kernel would silently reject and replace with
-        // the current time).
-        //
-        // This should be the same as (long) Math.ceil(mTimeToSendNanos / 1_000_000.0), except
-        // without the precision loss that comes from converting from long to double and back.
-        return mTimeToSendNanos / 1_000_000 + ((mTimeToSendNanos % 1_000_000 > 0) ? 1 : 0);
+        mTimeToSend = SystemClock.uptimeMillis();
     }
 
     /**
      * Inject uinput events to device
      *
      * @param events  Array of raw uinput events.
-     * @param offsetMicros The difference in microseconds between the timestamps of the previous
-     *                     batch of events injected and this batch. If set to -1, the current
-     *                     timestamp will be used.
      */
-    public void injectEvent(int[] events, long offsetMicros) {
+    public void injectEvent(int[] events) {
         // if two messages are sent at identical time, they will be processed in order received
-        SomeArgs args = SomeArgs.obtain();
-        args.arg1 = events;
-        args.argl1 = offsetMicros;
-        args.argl2 = mTimeToSendNanos;
-        Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, args);
-        mHandler.sendMessageAtTime(msg, getTimeToSendMillis());
+        Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, events);
+        mHandler.sendMessageAtTime(msg, mTimeToSend);
     }
 
     /**
-     * Delay subsequent device activity by the specified amount of time.
+     * Impose a delay to the device for execution.
      *
-     * <p>Note that although the delay is specified in nanoseconds, due to limitations of {@link
-     * Handler}'s API, scheduling only occurs with millisecond precision. When scheduling an
-     * injection or sync, the time at which it is scheduled will be rounded up to the nearest
-     * millisecond. While this means that a particular injection cannot be scheduled precisely,
-     * rounding errors will not accumulate over time. For example, if five injections are scheduled
-     * with a delay of 1,200,000ns before each one, the total delay will be 6ms, as opposed to the
-     * 10ms it would have been if each individual delay had been rounded up (as {@link EvemuParser}
-     * would otherwise have to do to avoid sending timestamps that are in the future).
-     *
-     * @param delayNanos  Time to delay in unit of nanoseconds.
+     * @param delay  Time to delay in unit of milliseconds.
      */
-    public void addDelayNanos(long delayNanos) {
-        mTimeToSendNanos += delayNanos;
+    public void addDelay(int delay) {
+        mTimeToSend = Math.max(SystemClock.uptimeMillis(), mTimeToSend) + delay;
     }
 
     /**
@@ -159,8 +131,7 @@
      * @param syncToken  The token for this sync command.
      */
     public void syncEvent(String syncToken) {
-        mHandler.sendMessageAtTime(
-                mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), getTimeToSendMillis());
+        mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), mTimeToSend);
     }
 
     /**
@@ -169,8 +140,7 @@
      */
     public void close() {
         Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE);
-        mHandler.sendMessageAtTime(
-                msg, Math.max(SystemClock.uptimeMillis(), getTimeToSendMillis()) + 1);
+        mHandler.sendMessageAtTime(msg, Math.max(SystemClock.uptimeMillis(), mTimeToSend) + 1);
         try {
             synchronized (mCond) {
                 mCond.wait();
@@ -181,7 +151,6 @@
 
     private class DeviceHandler extends Handler {
         private long mPtr;
-        private long mLastInjectTimestampMicros = -1;
         private int mBarrierToken;
 
         DeviceHandler(Looper looper) {
@@ -191,7 +160,7 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_OPEN_UINPUT_DEVICE: {
+                case MSG_OPEN_UINPUT_DEVICE:
                     SomeArgs args = (SomeArgs) msg.obj;
                     String name = (String) args.arg1;
                     mPtr = nativeOpenUinputDevice(name, args.argi1 /* id */,
@@ -208,44 +177,15 @@
                     }
                     args.recycle();
                     break;
-                }
-                case MSG_INJECT_EVENT: {
-                    SomeArgs args = (SomeArgs) msg.obj;
-                    if (mPtr == 0) {
-                        args.recycle();
-                        break;
+                case MSG_INJECT_EVENT:
+                    if (mPtr != 0) {
+                        int[] events = (int[]) msg.obj;
+                        for (int pos = 0; pos + 2 < events.length; pos += 3) {
+                            nativeInjectEvent(mPtr, events[pos], events[pos + 1], events[pos + 2]);
+                        }
                     }
-                    long offsetMicros = args.argl1;
-                    if (mLastInjectTimestampMicros == -1 || offsetMicros == -1) {
-                        // There's often a delay of a few milliseconds between the time specified to
-                        // Handler.sendMessageAtTime and the handler actually being called, due to
-                        // the way threads are scheduled. We don't take this into account when
-                        // calling addDelayNanos between the first batch of event injections (when
-                        // we set the "base timestamp" from which all others will be offset) and the
-                        // second batch, meaning that the actual time between the handler calls for
-                        // those batches may be less than the offset between their timestamps. When
-                        // that happens, we would pass a timestamp for the second batch that's
-                        // actually in the future. The kernel's uinput API rejects timestamps that
-                        // are in the future and uses the current time instead, making the reported
-                        // timestamps inconsistent with the recording we're replaying.
-                        //
-                        // To prevent this, we need to use the time we scheduled this first batch
-                        // for (in microseconds, to avoid potential rounding up from
-                        // getTimeToSendMillis), rather than the actual current time.
-                        mLastInjectTimestampMicros = args.argl2 / 1000;
-                    } else {
-                        mLastInjectTimestampMicros += offsetMicros;
-                    }
-
-                    int[] events = (int[]) args.arg1;
-                    for (int pos = 0; pos + 2 < events.length; pos += 3) {
-                        nativeInjectEvent(mPtr, mLastInjectTimestampMicros, events[pos],
-                                events[pos + 1], events[pos + 2]);
-                    }
-                    args.recycle();
                     break;
-                }
-                case MSG_CLOSE_UINPUT_DEVICE: {
+                case MSG_CLOSE_UINPUT_DEVICE:
                     if (mPtr != 0) {
                         nativeCloseUinputDevice(mPtr);
                         getLooper().quitSafely();
@@ -258,14 +198,11 @@
                         mCond.notify();
                     }
                     break;
-                }
-                case MSG_SYNC_EVENT: {
+                case MSG_SYNC_EVENT:
                     handleSyncEvent((String) msg.obj);
                     break;
-                }
-                default: {
+                default:
                     throw new IllegalArgumentException("Unknown device message");
-                }
             }
         }
 
diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
index da99162..7652f24 100644
--- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
@@ -44,7 +44,7 @@
      * recordings, this will always be the same.
      */
     private static final int DEVICE_ID = 1;
-    private static final int REGISTRATION_DELAY_NANOS = 500_000_000;
+    private static final int REGISTRATION_DELAY_MILLIS = 500;
 
     private static class CommentAwareReader {
         private final LineNumberReader mReader;
@@ -152,7 +152,7 @@
         final Event.Builder delayEb = new Event.Builder();
         delayEb.setId(DEVICE_ID);
         delayEb.setCommand(Event.Command.DELAY);
-        delayEb.setDurationNanos(REGISTRATION_DELAY_NANOS);
+        delayEb.setDurationMillis(REGISTRATION_DELAY_MILLIS);
         mQueuedEvents.add(delayEb.build());
     }
 
@@ -175,6 +175,7 @@
             throw new ParsingException(
                     "Invalid timestamp '" + parts[0] + "' (should contain a single '.')", mReader);
         }
+        // TODO(b/310958309): use timeMicros to set the timestamp on the event being sent.
         final long timeMicros =
                 parseLong(timeParts[0], 10) * 1_000_000 + parseInt(timeParts[1], 10);
         final Event.Builder eb = new Event.Builder();
@@ -191,18 +192,21 @@
             return eb.build();
         } else {
             final long delayMicros = timeMicros - mLastEventTimeMicros;
-            eb.setTimestampOffsetMicros(delayMicros);
-            if (delayMicros == 0) {
+            // The shortest delay supported by Handler.sendMessageAtTime (used for timings by the
+            // Device class) is 1ms, so ignore time differences smaller than that.
+            if (delayMicros < 1000) {
+                mLastEventTimeMicros = timeMicros;
                 return eb.build();
+            } else {
+                // Send a delay now, and queue the actual event for the next call.
+                mQueuedEvents.add(eb.build());
+                mLastEventTimeMicros = timeMicros;
+                final Event.Builder delayEb = new Event.Builder();
+                delayEb.setId(DEVICE_ID);
+                delayEb.setCommand(Event.Command.DELAY);
+                delayEb.setDurationMillis((int) (delayMicros / 1000));
+                return delayEb.build();
             }
-            // Send a delay now, and queue the actual event for the next call.
-            mQueuedEvents.add(eb.build());
-            mLastEventTimeMicros = timeMicros;
-            final Event.Builder delayEb = new Event.Builder();
-            delayEb.setId(DEVICE_ID);
-            delayEb.setCommand(Event.Command.DELAY);
-            delayEb.setDurationNanos(delayMicros * 1000);
-            return delayEb.build();
         }
     }
 
diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java
index 9e7ee09..0f16a27 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Event.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Event.java
@@ -99,9 +99,8 @@
     private int mVersionId;
     private int mBusId;
     private int[] mInjections;
-    private long mTimestampOffsetMicros = -1;
     private SparseArray<int[]> mConfiguration;
-    private long mDurationNanos;
+    private int mDurationMillis;
     private int mFfEffectsMax = 0;
     private String mInputPort;
     private SparseArray<InputAbsInfo> mAbsInfo;
@@ -140,28 +139,19 @@
     }
 
     /**
-     * Returns the number of microseconds that should be added to the previous {@code INJECT}
-     * event's timestamp to produce the timestamp for this {@code INJECT} event. A value of -1
-     * indicates that the current timestamp should be used instead.
-     */
-    public long getTimestampOffsetMicros() {
-        return mTimestampOffsetMicros;
-    }
-
-    /**
      * Returns a {@link SparseArray} describing the event codes that should be registered for the
      * device. The keys are uinput ioctl codes (such as those returned from {@link
      * UinputControlCode#getValue()}, while the values are arrays of event codes to be enabled with
      * those ioctls. For example, key 101 (corresponding to {@link UinputControlCode#UI_SET_KEYBIT})
-     * could have values 0x110 ({@code BTN_LEFT}), 0x111 ({@code BTN_RIGHT}), and 0x112
+     * could have values 0x110 ({@code BTN_LEFT}, 0x111 ({@code BTN_RIGHT}), and 0x112
      * ({@code BTN_MIDDLE}).
      */
     public SparseArray<int[]> getConfiguration() {
         return mConfiguration;
     }
 
-    public long getDurationNanos() {
-        return mDurationNanos;
+    public int getDurationMillis() {
+        return mDurationMillis;
     }
 
     public int getFfEffectsMax() {
@@ -192,7 +182,7 @@
             + ", busId=" + mBusId
             + ", events=" + Arrays.toString(mInjections)
             + ", configuration=" + mConfiguration
-            + ", duration=" + mDurationNanos + "ns"
+            + ", duration=" + mDurationMillis + "ms"
             + ", ff_effects_max=" + mFfEffectsMax
             + ", port=" + mInputPort
             + "}";
@@ -221,10 +211,6 @@
             mEvent.mInjections = events;
         }
 
-        public void setTimestampOffsetMicros(long offsetMicros) {
-            mEvent.mTimestampOffsetMicros = offsetMicros;
-        }
-
         /**
          * Sets the event codes that should be registered with a {@code register} command.
          *
@@ -251,8 +237,8 @@
             mEvent.mBusId = busId;
         }
 
-        public void setDurationNanos(long durationNanos) {
-            mEvent.mDurationNanos = durationNanos;
+        public void setDurationMillis(int durationMillis) {
+            mEvent.mDurationMillis = durationMillis;
         }
 
         public void setFfEffectsMax(int ffEffectsMax) {
@@ -285,7 +271,7 @@
                     }
                 }
                 case DELAY -> {
-                    if (mEvent.mDurationNanos <= 0) {
+                    if (mEvent.mDurationMillis <= 0) {
                         throw new IllegalStateException("Delay has missing or invalid duration");
                     }
                 }
diff --git a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
index 6994f0c..ed3ff33 100644
--- a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
@@ -71,8 +71,7 @@
                         case "configuration" -> eb.setConfiguration(readConfiguration());
                         case "ff_effects_max" -> eb.setFfEffectsMax(readInt());
                         case "abs_info" -> eb.setAbsInfo(readAbsInfoArray());
-                        // Duration is specified in milliseconds in the JSON-style format.
-                        case "duration" -> eb.setDurationNanos(readInt() * 1_000_000L);
+                        case "duration" -> eb.setDurationMillis(readInt());
                         case "port" -> eb.setInputPort(mReader.nextString());
                         case "syncToken" -> eb.setSyncToken(mReader.nextString());
                         default -> mReader.skipValue();
diff --git a/cmds/uinput/src/com/android/commands/uinput/Uinput.java b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
index 760e981..04df279 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Uinput.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
@@ -134,8 +134,8 @@
         switch (Objects.requireNonNull(e.getCommand())) {
             case REGISTER ->
                     error("Device id=" + e.getId() + " is already registered. Ignoring event.");
-            case INJECT -> d.injectEvent(e.getInjections(), e.getTimestampOffsetMicros());
-            case DELAY -> d.addDelayNanos(e.getDurationNanos());
+            case INJECT -> d.injectEvent(e.getInjections());
+            case DELAY -> d.addDelay(e.getDurationMillis());
             case SYNC -> d.syncEvent(e.getSyncToken());
         }
     }
diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
index 4dc4b68..a05cc67 100644
--- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
+++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
@@ -183,22 +183,16 @@
     }
 
     private void assertInjectEvent(Event event, int eventType, int eventCode, int value) {
-        assertInjectEvent(event, eventType, eventCode, value, 0);
-    }
-
-    private void assertInjectEvent(Event event, int eventType, int eventCode, int value,
-                                   long timestampOffsetMicros) {
         assertThat(event).isNotNull();
         assertThat(event.getCommand()).isEqualTo(Event.Command.INJECT);
         assertThat(event.getInjections()).asList()
                 .containsExactly(eventType, eventCode, value).inOrder();
-        assertThat(event.getTimestampOffsetMicros()).isEqualTo(timestampOffsetMicros);
     }
 
-    private void assertDelayEvent(Event event, int durationNanos) {
+    private void assertDelayEvent(Event event, int durationMillis) {
         assertThat(event).isNotNull();
         assertThat(event.getCommand()).isEqualTo(Event.Command.DELAY);
-        assertThat(event.getDurationNanos()).isEqualTo(durationNanos);
+        assertThat(event.getDurationMillis()).isEqualTo(durationMillis);
     }
 
     @Test
@@ -213,7 +207,7 @@
         EvemuParser parser = new EvemuParser(reader);
         assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.REGISTER);
         assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY);
-        assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1, -1);
+        assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1);
         assertInjectEvent(parser.getNextEvent(), 0x2, 0x1, -2);
         assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
     }
@@ -234,17 +228,17 @@
         assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.REGISTER);
         assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY);
 
-        assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, -1);
+        assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1);
         assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
 
-        assertDelayEvent(parser.getNextEvent(), 10_000_000);
+        assertDelayEvent(parser.getNextEvent(), 10);
 
-        assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0, 10_000);
+        assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0);
         assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
 
-        assertDelayEvent(parser.getNextEvent(), 1_000_000_000);
+        assertDelayEvent(parser.getNextEvent(), 1000);
 
-        assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, 1_000_000);
+        assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1);
         assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
     }
 
@@ -455,7 +449,7 @@
         assertThat(regEvent.getBus()).isEqualTo(0x001d);
         assertThat(regEvent.getVendorId()).isEqualTo(0x6cb);
         assertThat(regEvent.getProductId()).isEqualTo(0x0000);
-        // TODO(b/302297266): check version ID once it's supported
+        assertThat(regEvent.getVersionId()).isEqualTo(0x0000);
 
         assertThat(regEvent.getConfiguration().get(UinputControlCode.UI_SET_PROPBIT.getValue()))
                 .asList().containsExactly(0, 2);
@@ -483,7 +477,7 @@
 
         assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY);
 
-        assertInjectEvent(parser.getNextEvent(), 0x3, 0x39, 0, -1);
+        assertInjectEvent(parser.getNextEvent(), 0x3, 0x39, 0);
         assertInjectEvent(parser.getNextEvent(), 0x3, 0x35, 891);
         assertInjectEvent(parser.getNextEvent(), 0x3, 0x36, 333);
         assertInjectEvent(parser.getNextEvent(), 0x3, 0x3a, 56);
@@ -496,8 +490,8 @@
         assertInjectEvent(parser.getNextEvent(), 0x3, 0x18, 56);
         assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
 
-        assertDelayEvent(parser.getNextEvent(), 6_080_000);
+        assertDelayEvent(parser.getNextEvent(), 6);
 
-        assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888, 6_080);
+        assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888);
     }
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index 3fde9a6..18153a5 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1804,6 +1804,7 @@
     field public static final int useEmbeddedDex = 16844190; // 0x101059e
     field public static final int useIntrinsicSizeAsMinimum = 16843536; // 0x1010310
     field public static final int useLevel = 16843167; // 0x101019f
+    field @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public static final int useLocalePreferredLineHeightForMinimum;
     field public static final int userVisible = 16843409; // 0x1010291
     field public static final int usesCleartextTraffic = 16844012; // 0x10104ec
     field public static final int usesPermissionFlags = 16844356; // 0x1010644
@@ -3322,11 +3323,13 @@
     method @FlaggedApi("android.view.accessibility.a11y_overlay_callbacks") public void attachAccessibilityOverlayToWindow(int, @NonNull android.view.SurfaceControl, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
     method public boolean clearCache();
     method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void clearTestBrailleDisplayController();
     method public final void disableSelf();
     method public final boolean dispatchGesture(@NonNull android.accessibilityservice.GestureDescription, @Nullable android.accessibilityservice.AccessibilityService.GestureResultCallback, @Nullable android.os.Handler);
     method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
     method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController();
     method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(int);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @NonNull public android.accessibilityservice.BrailleDisplayController getBrailleDisplayController();
     method @NonNull @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController();
     method @Nullable public final android.accessibilityservice.InputMethod getInputMethod();
     method @NonNull public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController();
@@ -3356,6 +3359,7 @@
     method public boolean setCacheEnabled(boolean);
     method public void setGestureDetectionPassthroughRegion(int, @NonNull android.graphics.Region);
     method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void setTestBrailleDisplayController(@NonNull android.accessibilityservice.BrailleDisplayController);
     method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region);
     method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
     method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
@@ -3560,6 +3564,25 @@
     field public String[] packageNames;
   }
 
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void disconnect();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public boolean isConnected();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void write(@NonNull byte[]) throws java.io.IOException;
+  }
+
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public static interface BrailleDisplayController.BrailleDisplayCallback {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnected(@NonNull byte[]);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnectionFailed(int);
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onDisconnected();
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onInput(@NonNull byte[]);
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 2; // 0x2
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_CANNOT_ACCESS = 1; // 0x1
+  }
+
   public final class FingerprintGestureController {
     method public boolean isGestureDetectionAvailable();
     method public void registerFingerprintGestureCallback(@NonNull android.accessibilityservice.FingerprintGestureController.FingerprintGestureCallback, @Nullable android.os.Handler);
@@ -7817,6 +7840,7 @@
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.admin.DeviceAdminInfo> CREATOR;
     field public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1; // 0x1
+    field @FlaggedApi("android.app.admin.flags.headless_device_owner_single_user_enabled") public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; // 0x2
     field public static final int HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED = 0; // 0x0
     field public static final int USES_ENCRYPTED_STORAGE = 7; // 0x7
     field public static final int USES_POLICY_DISABLE_CAMERA = 8; // 0x8
@@ -25846,6 +25870,9 @@
     method public int describeContents();
     method public int getErrorCode();
     method public int getFinalState();
+    method @NonNull public java.util.List<android.media.metrics.MediaItemInfo> getInputMediaItemInfos();
+    method public long getOperationTypes();
+    method @Nullable public android.media.metrics.MediaItemInfo getOutputMediaItemInfo();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.EditingEndedEvent> CREATOR;
     field public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; // 0x12
@@ -25870,14 +25897,25 @@
     field public static final int FINAL_STATE_CANCELED = 2; // 0x2
     field public static final int FINAL_STATE_ERROR = 3; // 0x3
     field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1
+    field public static final long OPERATION_TYPE_AUDIO_EDIT = 8L; // 0x8L
+    field public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 2L; // 0x2L
+    field public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 32L; // 0x20L
+    field public static final long OPERATION_TYPE_PAUSED = 64L; // 0x40L
+    field public static final long OPERATION_TYPE_RESUMED = 128L; // 0x80L
+    field public static final long OPERATION_TYPE_VIDEO_EDIT = 4L; // 0x4L
+    field public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1L; // 0x1L
+    field public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 16L; // 0x10L
     field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff
   }
 
   @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder {
     ctor public EditingEndedEvent.Builder(int);
+    method @NonNull public android.media.metrics.EditingEndedEvent.Builder addInputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo);
+    method @NonNull public android.media.metrics.EditingEndedEvent.Builder addOperationType(long);
     method @NonNull public android.media.metrics.EditingEndedEvent build();
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int);
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle);
+    method @NonNull public android.media.metrics.EditingEndedEvent.Builder setOutputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo);
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long);
   }
 
@@ -25897,6 +25935,65 @@
     field @NonNull public static final android.media.metrics.LogSessionId LOG_SESSION_ID_NONE;
   }
 
+  @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public final class MediaItemInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAudioChannelCount();
+    method public long getAudioSampleCount();
+    method public int getAudioSampleRateHz();
+    method public long getClipDurationMillis();
+    method @NonNull public java.util.List<java.lang.String> getCodecNames();
+    method @Nullable public String getContainerMimeType();
+    method public long getDataTypes();
+    method public long getDurationMillis();
+    method @NonNull public java.util.List<java.lang.String> getSampleMimeTypes();
+    method public int getSourceType();
+    method public int getVideoDataSpace();
+    method public float getVideoFrameRate();
+    method public long getVideoSampleCount();
+    method @NonNull public android.util.Size getVideoSize();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.MediaItemInfo> CREATOR;
+    field public static final long DATA_TYPE_AUDIO = 4L; // 0x4L
+    field public static final long DATA_TYPE_CUE_POINTS = 128L; // 0x80L
+    field public static final long DATA_TYPE_DEPTH = 16L; // 0x10L
+    field public static final long DATA_TYPE_GAIN_MAP = 32L; // 0x20L
+    field public static final long DATA_TYPE_GAPLESS = 256L; // 0x100L
+    field public static final long DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO = 1024L; // 0x400L
+    field public static final long DATA_TYPE_HIGH_FRAME_RATE = 64L; // 0x40L
+    field public static final long DATA_TYPE_IMAGE = 1L; // 0x1L
+    field public static final long DATA_TYPE_METADATA = 8L; // 0x8L
+    field public static final long DATA_TYPE_SPATIAL_AUDIO = 512L; // 0x200L
+    field public static final long DATA_TYPE_VIDEO = 2L; // 0x2L
+    field public static final int SOURCE_TYPE_CAMERA = 2; // 0x2
+    field public static final int SOURCE_TYPE_EDITING_SESSION = 3; // 0x3
+    field public static final int SOURCE_TYPE_GALLERY = 1; // 0x1
+    field public static final int SOURCE_TYPE_GENERATED = 7; // 0x7
+    field public static final int SOURCE_TYPE_LOCAL_FILE = 4; // 0x4
+    field public static final int SOURCE_TYPE_REMOTE_FILE = 5; // 0x5
+    field public static final int SOURCE_TYPE_REMOTE_LIVE_STREAM = 6; // 0x6
+    field public static final int SOURCE_TYPE_UNSPECIFIED = 0; // 0x0
+    field public static final int VALUE_UNSPECIFIED = -1; // 0xffffffff
+  }
+
+  @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class MediaItemInfo.Builder {
+    ctor public MediaItemInfo.Builder();
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder addCodecName(@NonNull String);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder addDataType(long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder addSampleMimeType(@NonNull String);
+    method @NonNull public android.media.metrics.MediaItemInfo build();
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioChannelCount(@IntRange(from=0) int);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioSampleCount(@IntRange(from=0) long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioSampleRateHz(@IntRange(from=0) int);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setClipDurationMillis(long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setContainerMimeType(@NonNull String);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setDurationMillis(long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setSourceType(int);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoDataSpace(int);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoFrameRate(@FloatRange(from=0) float);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoSampleCount(@IntRange(from=0) long);
+    method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoSize(@NonNull android.util.Size);
+  }
+
   public final class MediaMetricsManager {
     method @NonNull public android.media.metrics.BundleSession createBundleSession();
     method @NonNull public android.media.metrics.EditingSession createEditingSession();
@@ -35463,7 +35560,7 @@
     field public static final String LONGITUDE = "longitude";
   }
 
-  @FlaggedApi("android.provider.user_keys") public class ContactKeysManager {
+  @FlaggedApi("android.provider.user_keys") public final class ContactKeysManager {
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.ContactKey> getAllContactKeys(@NonNull String);
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.SelfKey> getAllSelfKeys();
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public android.provider.ContactKeysManager.ContactKey getContactKey(@NonNull String, @NonNull String, @NonNull String);
@@ -35478,9 +35575,9 @@
     method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public void updateOrInsertContactKey(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
     method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateOrInsertSelfKey(@NonNull String, @NonNull String, @NonNull byte[]);
     method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, int);
-    field public static final int UNVERIFIED = 0; // 0x0
-    field public static final int VERIFICATION_FAILED = 1; // 0x1
-    field public static final int VERIFIED = 2; // 0x2
+    field public static final int VERIFICATION_STATE_UNVERIFIED = 0; // 0x0
+    field public static final int VERIFICATION_STATE_VERIFICATION_FAILED = 1; // 0x1
+    field public static final int VERIFICATION_STATE_VERIFIED = 2; // 0x2
   }
 
   public static final class ContactKeysManager.ContactKey implements android.os.Parcelable {
@@ -36977,6 +37074,7 @@
     field public static final String ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
     field public static final String ACTION_REQUEST_MANAGE_MEDIA = "android.settings.REQUEST_MANAGE_MEDIA";
     field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL";
+    field @FlaggedApi("android.provider.backup_tasks_settings_screen") public static final String ACTION_REQUEST_RUN_BACKUP_JOBS = "android.settings.REQUEST_RUN_BACKUP_JOBS";
     field public static final String ACTION_REQUEST_SCHEDULE_EXACT_ALARM = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM";
     field public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE = "android.settings.REQUEST_SET_AUTOFILL_SERVICE";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING";
@@ -52219,6 +52317,7 @@
     method public final boolean getClipToOutline();
     method @Nullable public final android.view.contentcapture.ContentCaptureSession getContentCaptureSession();
     method public CharSequence getContentDescription();
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final int getContentSensitivity();
     method @UiContext public final android.content.Context getContext();
     method protected android.view.ContextMenu.ContextMenuInfo getContextMenuInfo();
     method public final boolean getDefaultFocusHighlightEnabled();
@@ -52398,6 +52497,7 @@
     method public boolean isAttachedToWindow();
     method public boolean isAutoHandwritingEnabled();
     method public boolean isClickable();
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final boolean isContentSensitive();
     method public boolean isContextClickable();
     method public boolean isCredential();
     method public boolean isDirty();
@@ -52602,6 +52702,7 @@
     method public void setClipToOutline(boolean);
     method public void setContentCaptureSession(@Nullable android.view.contentcapture.ContentCaptureSession);
     method public void setContentDescription(CharSequence);
+    method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final void setContentSensitivity(int);
     method public void setContextClickable(boolean);
     method public void setDefaultFocusHighlightEnabled(boolean);
     method @Deprecated public void setDrawingCacheBackgroundColor(@ColorInt int);
@@ -52786,6 +52887,9 @@
     field public static final int AUTOFILL_TYPE_NONE = 0; // 0x0
     field public static final int AUTOFILL_TYPE_TEXT = 1; // 0x1
     field public static final int AUTOFILL_TYPE_TOGGLE = 2; // 0x2
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_AUTO = 0; // 0x0
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 2; // 0x2
+    field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_SENSITIVE = 1; // 0x1
     field public static final int DRAG_FLAG_ACCESSIBILITY_ACTION = 1024; // 0x400
     field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100
     field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40
@@ -60506,6 +60610,7 @@
     method public boolean isFallbackLineSpacing();
     method public final boolean isHorizontallyScrollable();
     method public boolean isInputMethodTarget();
+    method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public boolean isLocalePreferredLineHeightForMinimumUsed();
     method public boolean isSingleLine();
     method public boolean isSuggestionsEnabled();
     method public boolean isTextSelectable();
@@ -60588,6 +60693,7 @@
     method public final void setLinkTextColor(@ColorInt int);
     method public final void setLinkTextColor(android.content.res.ColorStateList);
     method public final void setLinksClickable(boolean);
+    method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void setLocalePreferredLineHeightForMinimumUsed(boolean);
     method public void setMarqueeRepeatLimit(int);
     method public void setMaxEms(int);
     method public void setMaxHeight(int);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 50c9c33..18f955f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -100,7 +100,6 @@
     field public static final String CAMERA_DISABLE_TRANSMIT_LED = "android.permission.CAMERA_DISABLE_TRANSMIT_LED";
     field @FlaggedApi("com.android.internal.camera.flags.camera_hsum_permission") public static final String CAMERA_HEADLESS_SYSTEM_USER = "android.permission.CAMERA_HEADLESS_SYSTEM_USER";
     field public static final String CAMERA_OPEN_CLOSE_LISTENER = "android.permission.CAMERA_OPEN_CLOSE_LISTENER";
-    field @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") public static final String CAMERA_PRIVACY_ALLOWLIST = "android.permission.CAMERA_PRIVACY_ALLOWLIST";
     field public static final String CAPTURE_AUDIO_HOTWORD = "android.permission.CAPTURE_AUDIO_HOTWORD";
     field public static final String CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD = "android.permission.CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD";
     field public static final String CAPTURE_MEDIA_OUTPUT = "android.permission.CAPTURE_MEDIA_OUTPUT";
@@ -1390,6 +1389,7 @@
     field public static final int STATUS_DEVICE_ADMIN_NOT_SUPPORTED = 13; // 0xd
     field public static final int STATUS_HAS_DEVICE_OWNER = 1; // 0x1
     field public static final int STATUS_HAS_PAIRED = 8; // 0x8
+    field @FlaggedApi("android.app.admin.flags.headless_device_owner_single_user_enabled") public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17; // 0x11
     field public static final int STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED = 16; // 0x10
     field public static final int STATUS_MANAGED_USERS_NOT_SUPPORTED = 9; // 0x9
     field public static final int STATUS_NONSYSTEM_USER_EXISTS = 5; // 0x5
@@ -4360,10 +4360,12 @@
   public final class DomainVerificationManager {
     method @Nullable @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public android.content.pm.verify.domain.DomainVerificationInfo getDomainVerificationInfo(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
     method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public java.util.SortedSet<android.content.pm.verify.domain.DomainOwner> getOwnersForDomain(@NonNull String);
+    method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @NonNull public java.util.Map<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>> getUriRelativeFilterGroups(@NonNull String, @NonNull java.util.List<java.lang.String>);
     method @NonNull @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public java.util.List<java.lang.String> queryValidVerificationPackageNames();
     method @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public void setDomainVerificationLinkHandlingAllowed(@NonNull String, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
     method @CheckResult @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public int setDomainVerificationStatus(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, int) throws android.content.pm.PackageManager.NameNotFoundException;
     method @CheckResult @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public int setDomainVerificationUserSelection(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
+    method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public void setUriRelativeFilterGroups(@NonNull String, @NonNull java.util.Map<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>>);
     field public static final int ERROR_DOMAIN_SET_ID_INVALID = 1; // 0x1
     field public static final int ERROR_UNABLE_TO_APPROVE = 3; // 0x3
     field public static final int ERROR_UNKNOWN_DOMAIN = 2; // 0x2
@@ -4657,15 +4659,11 @@
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void addSensorPrivacyListener(@NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void addSensorPrivacyListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean areAnySensorPrivacyTogglesEnabled(int);
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @NonNull @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public java.util.Map<java.lang.String,java.lang.Boolean> getCameraPrivacyAllowlist();
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public int getSensorPrivacyState(int, int);
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isCameraPrivacyEnabled(@NonNull String);
     method @Deprecated @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isSensorPrivacyEnabled(int);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isSensorPrivacyEnabled(int, int);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void removeSensorPrivacyListener(int, @NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void removeSensorPrivacyListener(@NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener);
     method @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacy(int, boolean);
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacyState(int, int);
   }
 
   public static interface SensorPrivacyManager.OnSensorPrivacyChangedListener {
@@ -4675,7 +4673,6 @@
 
   public static class SensorPrivacyManager.OnSensorPrivacyChangedListener.SensorPrivacyChangedParams {
     method public int getSensor();
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") public int getState();
     method public int getToggleType();
     method public boolean isEnabled();
   }
@@ -11068,6 +11065,7 @@
     field public static final String USER_TYPE_FULL_SYSTEM = "android.os.usertype.full.SYSTEM";
     field public static final String USER_TYPE_PROFILE_CLONE = "android.os.usertype.profile.CLONE";
     field public static final String USER_TYPE_PROFILE_MANAGED = "android.os.usertype.profile.MANAGED";
+    field @FlaggedApi("android.os.allow_private_profile") public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE";
     field public static final String USER_TYPE_SYSTEM_HEADLESS = "android.os.usertype.system.HEADLESS";
   }
 
@@ -11471,7 +11469,7 @@
     field public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
-  @FlaggedApi("android.provider.user_keys") public class ContactKeysManager {
+  @FlaggedApi("android.provider.user_keys") public final class ContactKeysManager {
     method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyLocalVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int);
     method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int);
     method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, int);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1e30a32..a7f80dd 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -104,6 +104,14 @@
     method @FlaggedApi("android.view.accessibility.motion_event_observing") public void setObservedMotionEventSources(int);
   }
 
+  @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
+    method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public static void setTestBrailleDisplayData(@NonNull android.accessibilityservice.AccessibilityService, @NonNull java.util.List<android.os.Bundle>);
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
+    field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+  }
+
 }
 
 package android.animation {
@@ -1483,7 +1491,6 @@
 
   public final class SensorPrivacyManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacy(int, int, boolean);
-    method @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacyState(int, int, int);
   }
 
   public static class SensorPrivacyManager.Sources {
@@ -2455,7 +2462,6 @@
     method public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported();
     method public boolean isVisibleBackgroundUsersSupported();
     method @Deprecated @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException;
-    field @FlaggedApi("android.os.allow_private_profile") public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE";
   }
 
   public final class VibrationAttributes implements android.os.Parcelable {
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index e7e3a85..f7d7522 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -80,6 +80,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
@@ -850,6 +851,8 @@
     private boolean mInputMethodInitialized = false;
     private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers =
             new SparseArray<>(0);
+    private BrailleDisplayController mBrailleDisplayController;
+    private BrailleDisplayController mTestBrailleDisplayController;
 
     private int mGestureStatusCallbackSequence;
 
@@ -3634,4 +3637,56 @@
                 .attachAccessibilityOverlayToWindow(
                         mConnectionId, accessibilityWindowId, sc, executor, callback);
     }
+
+    /**
+     * Returns the {@link BrailleDisplayController} which may be used to communicate with
+     * refreshable Braille displays that provide USB or Bluetooth Braille display HID support.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @NonNull
+    public BrailleDisplayController getBrailleDisplayController() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            if (mTestBrailleDisplayController != null) {
+                return mTestBrailleDisplayController;
+            }
+
+            if (mBrailleDisplayController == null) {
+                mBrailleDisplayController = new BrailleDisplayControllerImpl(this, mLock);
+            }
+            return mBrailleDisplayController;
+        }
+    }
+
+    /**
+     * Set the {@link BrailleDisplayController} implementation that will be returned by
+     * {@link #getBrailleDisplayController}, to allow this accessibility service to test its
+     * interaction with BrailleDisplayController without requiring a real Braille display.
+     *
+     * <p>For full test fidelity, ensure that this test-only implementation follows the same
+     * behavior specified in the documentation for {@link BrailleDisplayController}, including
+     * thrown exceptions.
+     *
+     * @param controller A test-only implementation of {@link BrailleDisplayController}.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void setTestBrailleDisplayController(@NonNull BrailleDisplayController controller) {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        Objects.requireNonNull(controller);
+        synchronized (mLock) {
+            mTestBrailleDisplayController = controller;
+        }
+    }
+
+    /**
+     * Clears the {@link BrailleDisplayController} previously set by
+     * {@link #setTestBrailleDisplayController}.
+     */
+    @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void clearTestBrailleDisplayController() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            mTestBrailleDisplayController = null;
+        }
+    }
 }
diff --git a/core/java/android/accessibilityservice/BrailleDisplayController.java b/core/java/android/accessibilityservice/BrailleDisplayController.java
new file mode 100644
index 0000000..5282aa3
--- /dev/null
+++ b/core/java/android/accessibilityservice/BrailleDisplayController.java
@@ -0,0 +1,308 @@
+/*
+ * 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.accessibilityservice;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.Flags;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Used to communicate with a Braille display that supports the Braille display HID standard
+ * (usage page 0x41).
+ *
+ * <p>Only one Braille display may be connected at a time.
+ */
+// This interface doesn't actually own resources. Its I/O connections are owned, monitored,
+// and automatically closed by the system after the accessibility service is disconnected.
+@SuppressLint("NotCloseable")
+@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+public interface BrailleDisplayController {
+
+    /**
+     * Throw {@link IllegalStateException} if this feature's aconfig flag is disabled.
+     *
+     * @hide
+     */
+    static void checkApiFlagIsEnabled() {
+        if (!Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+    }
+
+    /**
+     * Interface provided to {@link BrailleDisplayController} connection methods to
+     * receive callbacks from the system.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    interface BrailleDisplayCallback {
+        /**
+         * The system cannot access connected HID devices.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        int FLAG_ERROR_CANNOT_ACCESS = 1 << 0;
+        /**
+         * A unique Braille display matching the requested properties could not be identified.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 1 << 1;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = "FLAG_ERROR_", value = {
+                FLAG_ERROR_CANNOT_ACCESS,
+                FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND,
+        })
+        @interface ErrorCode {
+        }
+
+        /**
+         * Callback to observe a successful Braille display connection.
+         *
+         * <p>The provided HID report descriptor should be used to understand the input bytes
+         * received from the Braille display via {@link #onInput} and to prepare
+         * the output sent to the Braille display via {@link #write}.
+         *
+         * @param hidDescriptor The HID report descriptor for this Braille display.
+         * @see #connect(BluetoothDevice, BrailleDisplayCallback)
+         * @see #connect(UsbDevice, BrailleDisplayCallback)
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onConnected(@NonNull byte[] hidDescriptor);
+
+        /**
+         * Callback to observe a failed Braille display connection.
+         *
+         * @param errorFlags A bitmask of error codes for the connection failure.
+         * @see #connect(BluetoothDevice, BrailleDisplayCallback)
+         * @see #connect(UsbDevice, BrailleDisplayCallback)
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onConnectionFailed(@ErrorCode int errorFlags);
+
+        /**
+         * Callback to observe input bytes from the currently connected Braille display.
+         *
+         * @param input The input bytes from the Braille display, formatted according to the HID
+         *              report descriptor and the HIDRAW kernel driver.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onInput(@NonNull byte[] input);
+
+        /**
+         * Callback to observe when the currently connected Braille display is disconnected by the
+         * system.
+         */
+        @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+        void onDisconnected();
+    }
+
+    /**
+     * Connects to the requested bluetooth Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>Note that the callbacks will be executed on the main thread using
+     * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
+     * {@link #connect(BluetoothDevice, Executor, BrailleDisplayCallback)}.
+     *
+     * @param bluetoothDevice The Braille display device.
+     * @param callback        Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested bluetooth Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * @param bluetoothDevice  The Braille display device.
+     * @param callbackExecutor Executor for executing the provided callbacks.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested USB Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>The accessibility service app must already have approval to access the USB device
+     * from the standard {@link android.hardware.usb.UsbManager} access approval process.
+     *
+     * <p>Note that the callbacks will be executed on the main thread using
+     * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
+     * {@link #connect(UsbDevice, Executor, BrailleDisplayCallback)}.
+     *
+     * @param usbDevice        The Braille display device.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws SecurityException if the caller does not have USB device approval.
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void connect(@NonNull UsbDevice usbDevice,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Connects to the requested USB Braille display using the Braille
+     * display HID standard (usage page 0x41).
+     *
+     * <p>If successful then the HID report descriptor will be provided to
+     * {@link BrailleDisplayCallback#onConnected}
+     * and the Braille display will start sending incoming input bytes to
+     * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+     * then the system will disconnect the Braille display.
+     *
+     * <p>The accessibility service app must already have approval to access the USB device
+     * from the standard {@link android.hardware.usb.UsbManager} access approval process.
+     *
+     * @param usbDevice        The Braille display device.
+     * @param callbackExecutor Executor for executing the provided callbacks.
+     * @param callback         Callbacks used to provide connection results.
+     * @see BrailleDisplayCallback#onConnected
+     * @see BrailleDisplayCallback#onConnectionFailed
+     * @throws SecurityException if the caller does not have USB device approval.
+     * @throws IllegalStateException if a Braille display is already connected to this controller.
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void connect(@NonNull UsbDevice usbDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback);
+
+    /**
+     * Returns true if a Braille display is currently connected, otherwise false.
+     *
+     * @see #connect
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    boolean isConnected();
+
+    /**
+     * Writes a HID report to the currently connected Braille display.
+     *
+     * <p>This method returns immediately after dispatching the write request to the system.
+     * If the system experiences an error in writing output (e.g. the Braille display is unplugged
+     * after the system receives the write request but before writing the bytes to the Braille
+     * display) then the system will disconnect the Braille display, which calls
+     * {@link BrailleDisplayCallback#onDisconnected()}.
+     *
+     * @param buffer The bytes to write to the Braille display. These bytes should be formatted
+     *               according to the HID report descriptor and the HIDRAW kernel driver.
+     * @throws IOException              if there is no currently connected Braille display.
+     * @throws IllegalArgumentException if the buffer exceeds the maximum safe payload size for
+     *                                  binder transactions of
+     *                                  {@link IBinder#getSuggestedMaxIpcSizeBytes()}
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void write(@NonNull byte[] buffer) throws IOException;
+
+    /**
+     * Disconnects from the currently connected Braille display.
+     *
+     * @see #isConnected()
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    void disconnect();
+
+    /**
+     * Provides test Braille display data to be used for automated CTS tests.
+     *
+     * <p>See {@code TEST_BRAILLE_DISPLAY_*} bundle keys.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)
+    @TestApi
+    static void setTestBrailleDisplayData(
+            @NonNull AccessibilityService service,
+            @NonNull List<Bundle> brailleDisplays) {
+        checkApiFlagIsEnabled();
+        final IAccessibilityServiceConnection serviceConnection =
+                AccessibilityInteractionClient.getConnection(service.getConnectionId());
+        if (serviceConnection != null) {
+            try {
+                serviceConnection.setTestBrailleDisplayData(brailleDisplays);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+    @TestApi
+    String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+}
diff --git a/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java
new file mode 100644
index 0000000..cac1dc4
--- /dev/null
+++ b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java
@@ -0,0 +1,267 @@
+/*
+ * 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.accessibilityservice;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.Flags;
+
+import com.android.internal.util.FunctionalUtils;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Default implementation of {@link BrailleDisplayController}.
+ */
+// BrailleDisplayControllerImpl is not an API, but it implements BrailleDisplayController APIs.
+// This @FlaggedApi annotation tells the linter that this method delegates API checks to its
+// callers.
+@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+final class BrailleDisplayControllerImpl implements BrailleDisplayController {
+
+    private final AccessibilityService mAccessibilityService;
+    private final Object mLock;
+
+    private IBrailleDisplayConnection mBrailleDisplayConnection;
+    private Executor mCallbackExecutor;
+    private BrailleDisplayCallback mCallback;
+
+    BrailleDisplayControllerImpl(AccessibilityService accessibilityService,
+            Object lock) {
+        mAccessibilityService = accessibilityService;
+        mLock = lock;
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    public void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull BrailleDisplayCallback callback) {
+        connect(bluetoothDevice, mAccessibilityService.getMainExecutor(), callback);
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+    public void connect(@NonNull BluetoothDevice bluetoothDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback) {
+        Objects.requireNonNull(bluetoothDevice);
+        Objects.requireNonNull(callbackExecutor);
+        Objects.requireNonNull(callback);
+        connect(serviceConnection -> serviceConnection.connectBluetoothBrailleDisplay(
+                        bluetoothDevice.getAddress(), new IBrailleDisplayControllerWrapper()),
+                callbackExecutor, callback);
+    }
+
+    @Override
+    public void connect(@NonNull UsbDevice usbDevice,
+            @NonNull BrailleDisplayCallback callback) {
+        connect(usbDevice, mAccessibilityService.getMainExecutor(), callback);
+    }
+
+    @Override
+    public void connect(@NonNull UsbDevice usbDevice,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull BrailleDisplayCallback callback) {
+        Objects.requireNonNull(usbDevice);
+        Objects.requireNonNull(callbackExecutor);
+        Objects.requireNonNull(callback);
+        connect(serviceConnection -> serviceConnection.connectUsbBrailleDisplay(
+                        usbDevice, new IBrailleDisplayControllerWrapper()),
+                callbackExecutor, callback);
+    }
+
+    /**
+     * Shared implementation for the {@code connect()} API methods.
+     *
+     * <p>Performs a blocking call to system_server to create the connection. Success is
+     * returned through {@link BrailleDisplayCallback#onConnected} while normal connection
+     * errors are returned through {@link BrailleDisplayCallback#onConnectionFailed}. This
+     * connection is implemented using cached data from the HIDRAW driver so it returns
+     * quickly without needing to perform any I/O with the Braille display.
+     *
+     * <p>The AIDL call to system_server is blocking (not posted to a handler thread) so
+     * that runtime exceptions signaling abnormal connection errors from API misuse
+     * (e.g. lacking permissions, providing an invalid BluetoothDevice, calling connect
+     * while already connected) are propagated to the API caller.
+     */
+    private void connect(
+            FunctionalUtils.RemoteExceptionIgnoringConsumer<IAccessibilityServiceConnection>
+                    createConnection,
+            @NonNull Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        if (isConnected()) {
+            throw new IllegalStateException(
+                    "This service already has a connected Braille display");
+        }
+        final IAccessibilityServiceConnection serviceConnection =
+                AccessibilityInteractionClient.getConnection(
+                        mAccessibilityService.getConnectionId());
+        if (serviceConnection == null) {
+            throw new IllegalStateException("Accessibility service is not connected");
+        }
+        synchronized (mLock) {
+            mCallbackExecutor = callbackExecutor;
+            mCallback = callback;
+        }
+        try {
+            createConnection.acceptOrThrow(serviceConnection);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public boolean isConnected() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        return mBrailleDisplayConnection != null;
+    }
+
+    @Override
+    public void write(@NonNull byte[] buffer) throws IOException {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        Objects.requireNonNull(buffer);
+        if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
+            // This same check must be performed in the system to prevent reflection misuse,
+            // but perform it here too to prevent unnecessary IPCs from non-reflection callers.
+            throw new IllegalArgumentException("Invalid write buffer size " + buffer.length);
+        }
+        synchronized (mLock) {
+            if (mBrailleDisplayConnection == null) {
+                throw new IOException("Braille display is not connected");
+            }
+            try {
+                mBrailleDisplayConnection.write(buffer);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        BrailleDisplayController.checkApiFlagIsEnabled();
+        synchronized (mLock) {
+            try {
+                if (mBrailleDisplayConnection != null) {
+                    mBrailleDisplayConnection.disconnect();
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } finally {
+                clearConnectionLocked();
+            }
+        }
+    }
+
+    /**
+     * Implementation of the {@code IBrailleDisplayController} AIDL interface provided to
+     * system_server, which system_server uses to pass messages back to this
+     * {@code BrailleDisplayController}.
+     *
+     * <p>Messages from system_server are routed to the {@link BrailleDisplayCallback} callbacks
+     * implemented by the accessibility service.
+     *
+     * <p>Note: Per API Guidelines 7.5 the Binder identity must be cleared before invoking the
+     * callback executor so that Binder identity checks in the callbacks are performed using the
+     * app's identity.
+     */
+    private final class IBrailleDisplayControllerWrapper extends IBrailleDisplayController.Stub {
+        /**
+         * Called when the system successfully connects to a Braille display.
+         */
+        @Override
+        public void onConnected(IBrailleDisplayConnection connection, byte[] hidDescriptor) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mBrailleDisplayConnection = connection;
+                    mCallbackExecutor.execute(() -> mCallback.onConnected(hidDescriptor));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when the system is unable to connect to a Braille display.
+         */
+        @Override
+        public void onConnectionFailed(@BrailleDisplayCallback.ErrorCode int errorCode) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mCallbackExecutor.execute(() -> mCallback.onConnectionFailed(errorCode));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when input is received from the currently connected Braille display.
+         */
+        @Override
+        public void onInput(byte[] input) {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    // Ignore input that arrives after disconnection.
+                    if (mBrailleDisplayConnection != null) {
+                        mCallbackExecutor.execute(() -> mCallback.onInput(input));
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        /**
+         * Called when the currently connected Braille display is disconnected.
+         */
+        @Override
+        public void onDisconnected() {
+            BrailleDisplayController.checkApiFlagIsEnabled();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mCallbackExecutor.execute(mCallback::onDisconnected);
+                    clearConnectionLocked();
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    private void clearConnectionLocked() {
+        mBrailleDisplayConnection = null;
+    }
+
+}
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 96716db..dc5c7f6 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -17,10 +17,12 @@
 package android.accessibilityservice;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Bitmap;
 import android.graphics.Region;
+import android.hardware.usb.UsbDevice;
 import android.os.Bundle;
 import android.os.RemoteCallback;
 import android.view.MagnificationSpec;
@@ -160,4 +162,12 @@
     void attachAccessibilityOverlayToDisplay(int interactionId, int displayId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback);
 
     void attachAccessibilityOverlayToWindow(int interactionId, int accessibilityWindowId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+    void connectBluetoothBrailleDisplay(in String bluetoothAddress, in IBrailleDisplayController controller);
+
+    void connectUsbBrailleDisplay(in UsbDevice usbDevice, in IBrailleDisplayController controller);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
+    void setTestBrailleDisplayData(in List<Bundle> brailleDisplays);
 }
\ No newline at end of file
diff --git a/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl
new file mode 100644
index 0000000..ec4d7b1
--- /dev/null
+++ b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl
@@ -0,0 +1,12 @@
+package android.accessibilityservice;
+
+/**
+ * Interface given to a BrailleDisplayController to talk to a BrailleDisplayConnection
+ * in system_server.
+ *
+ * @hide
+ */
+interface IBrailleDisplayConnection {
+    oneway void disconnect();
+    oneway void write(in byte[] output);
+}
\ No newline at end of file
diff --git a/core/java/android/accessibilityservice/IBrailleDisplayController.aidl b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl
new file mode 100644
index 0000000..7a5d83e
--- /dev/null
+++ b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl
@@ -0,0 +1,17 @@
+package android.accessibilityservice;
+
+import android.accessibilityservice.IBrailleDisplayConnection;
+
+/**
+ * Interface given to a BrailleDisplayConnection to talk to a BrailleDisplayController
+ * in an accessibility service.
+ *
+ * IPCs from system_server to apps must be oneway, so designate this entire interface as oneway.
+ * @hide
+ */
+oneway interface IBrailleDisplayController {
+    void onConnected(in IBrailleDisplayConnection connection, in byte[] hidDescriptor);
+    void onConnectionFailed(int error);
+    void onInput(in byte[] input);
+    void onDisconnected();
+}
\ No newline at end of file
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 14462b8..7d5d5c1 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -16,8 +16,10 @@
 
 package android.app.admin;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.app.admin.flags.Flags;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -176,7 +178,18 @@
      */
     public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1;
 
-    @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED})
+    /**
+     * Value for {@link #getHeadlessDeviceOwnerMode} which indicates that this DPC should be
+     * provisioned into the first secondary user when on a Headless System User Mode device.
+     *
+     * <p>This mode only allows a single secondary user on the device blocking the creation of
+     * additional secondary users.
+     */
+    @FlaggedApi(Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
+    public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2;
+
+    @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED,
+            HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER})
     @Retention(RetentionPolicy.SOURCE)
     private @interface HeadlessDeviceOwnerMode {}
 
@@ -373,6 +386,8 @@
                         mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
                     } else if (deviceOwnerModeStringValue.equalsIgnoreCase("affiliated")) {
                         mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
+                    } else if (deviceOwnerModeStringValue.equalsIgnoreCase("single_user")) {
+                        mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
                     } else {
                         throw new XmlPullParserException("headless-system-user mode must be valid");
                     }
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index c8762c6..c649e62 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -84,6 +84,7 @@
 import android.app.IServiceConnection;
 import android.app.KeyguardManager;
 import android.app.admin.SecurityLog.SecurityEvent;
+import android.app.admin.flags.Flags;
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
@@ -2863,6 +2864,19 @@
     public static final int STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED = 16;
 
     /**
+     * Result code for {@link #checkProvisioningPrecondition}.
+     *
+     * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} when provisioning a DPC into the
+     * {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER} mode but only the system
+     * user exists on the device.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
+    public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17;
+
+    /**
      * Result codes for {@link #checkProvisioningPrecondition} indicating all the provisioning pre
      * conditions.
      *
@@ -2876,7 +2890,7 @@
             STATUS_CANNOT_ADD_MANAGED_PROFILE, STATUS_DEVICE_ADMIN_NOT_SUPPORTED,
             STATUS_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER,
             STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS,
-            STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED
+            STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED, STATUS_HEADLESS_ONLY_SYSTEM_USER
     })
     public @interface ProvisioningPrecondition {}
 
@@ -9178,9 +9192,11 @@
      * <p>Calling this after the setup phase of the device owner user has completed is allowed only
      * if the caller is the {@link Process#SHELL_UID Shell UID}, and there are no additional users
      * (except when the device runs on headless system user mode, in which case it could have exact
-     * one extra user, which is the current user - the device owner will be set in the
-     * {@link UserHandle#SYSTEM system} user and a profile owner will be set in the current user)
-     * and no accounts.
+     * one extra user, which is the current user.
+     *
+     * <p>On a headless devices, if it is in affiliated mode the device owner will be set in the
+     * {@link UserHandle#SYSTEM system} user. If the device is in single user mode, the device owner
+     * will be set in the first secondary user.
      *
      * @param who the component name to be registered as device owner.
      * @param userId ID of the user on which the device owner runs.
@@ -11371,7 +11387,9 @@
      * @see UserHandle
      * @return the {@link android.os.UserHandle} object for the created user, or {@code null} if the
      *         user could not be created.
-     * @throws SecurityException if {@code admin} is not a device owner.
+     * @throws SecurityException if headless device is in
+     *        {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER} mode.
+     * @throws SecurityException if {@code admin} is not a device owner
      * @throws UserOperationException if the user could not be created and the calling app is
      * targeting {@link android.os.Build.VERSION_CODES#P} and running on
      * {@link android.os.Build.VERSION_CODES#P}.
@@ -16612,7 +16630,10 @@
      * before calling this method.
      *
      * <p>Holders of {@link android.Manifest.permission#PROVISION_DEMO_DEVICE} can call this API
-     * only if {@link FullyManagedDeviceProvisioningParams#isDemoDevice()} is {@code true}.</p>
+     * only if {@link FullyManagedDeviceProvisioningParams#isDemoDevice()} is {@code true}.
+     *
+     * <p>If headless device is in {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER}
+     * mode then it sets the device owner on the first secondary user.</p>
      *
      * @param provisioningParams Params required to provision a fully managed device,
      * see {@link FullyManagedDeviceProvisioningParams}.
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 6cc8af8..3c98ef9 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -76,3 +76,10 @@
   description: "Enable APIs to provision and manage eSIMs"
   bug: "295301164"
 }
+
+flag {
+  name: "headless_device_owner_single_user_enabled"
+  namespace: "enterprise"
+  description: "Add Headless DO support."
+  bug: "289515470"
+}
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
index 79af65a..d913581 100644
--- a/core/java/android/content/IntentFilter.java
+++ b/core/java/android/content/IntentFilter.java
@@ -559,6 +559,10 @@
             sb.append(" sch=");
             sb.append(mDataSchemes.toString());
         }
+        if (Flags.relativeReferenceIntentFilters() && countUriRelativeFilterGroups() > 0) {
+            sb.append(" grp=");
+            sb.append(mUriRelativeFilterGroups.toString());
+        }
         sb.append(" }");
         return sb.toString();
     }
@@ -1807,13 +1811,7 @@
         if (mUriRelativeFilterGroups == null) {
             return false;
         }
-        for (int i = 0; i < mUriRelativeFilterGroups.size(); i++) {
-            UriRelativeFilterGroup group = mUriRelativeFilterGroups.get(i);
-            if (group.matchData(data)) {
-                return group.getAction() == UriRelativeFilterGroup.ACTION_ALLOW;
-            }
-        }
-        return false;
+        return UriRelativeFilterGroup.matchGroupsToUri(mUriRelativeFilterGroups, data);
     }
 
     /**
diff --git a/core/java/android/content/UriRelativeFilter.java b/core/java/android/content/UriRelativeFilter.java
index 9866cd0..6d33246 100644
--- a/core/java/android/content/UriRelativeFilter.java
+++ b/core/java/android/content/UriRelativeFilter.java
@@ -217,6 +217,15 @@
                 + " }";
     }
 
+    /** @hide */
+    public UriRelativeFilterParcel toParcel() {
+        UriRelativeFilterParcel parcel = new UriRelativeFilterParcel();
+        parcel.uriPart = mUriPart;
+        parcel.patternType = mPatternType;
+        parcel.filter = mFilter;
+        return parcel;
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
         if (this == o) return true;
@@ -257,4 +266,11 @@
         mPatternType = Integer.parseInt(parser.getAttributeValue(null, PATTERN_STR));
         mFilter = parser.getAttributeValue(null, FILTER_STR);
     }
+
+    /** @hide */
+    public UriRelativeFilter(UriRelativeFilterParcel parcel) {
+        mUriPart = parcel.uriPart;
+        mPatternType = parcel.patternType;
+        mFilter = parcel.filter;
+    }
 }
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/core/java/android/content/UriRelativeFilterGroup.aidl
similarity index 81%
rename from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
rename to core/java/android/content/UriRelativeFilterGroup.aidl
index 838e41e..b251054 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/core/java/android/content/UriRelativeFilterGroup.aidl
@@ -14,11 +14,6 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package android.content;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
-}
-
+parcelable UriRelativeFilterGroup;
diff --git a/core/java/android/content/UriRelativeFilterGroup.java b/core/java/android/content/UriRelativeFilterGroup.java
index 72c396a..0e49b4f 100644
--- a/core/java/android/content/UriRelativeFilterGroup.java
+++ b/core/java/android/content/UriRelativeFilterGroup.java
@@ -19,6 +19,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.pm.Flags;
 import android.net.Uri;
 import android.os.Parcel;
@@ -36,9 +37,11 @@
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -83,6 +86,40 @@
     private final @Action int mAction;
     private final ArraySet<UriRelativeFilter> mUriRelativeFilters = new ArraySet<>();
 
+    /** @hide */
+    public static boolean matchGroupsToUri(List<UriRelativeFilterGroup> groups, Uri uri) {
+        for (int i = 0; i < groups.size(); i++) {
+            if (groups.get(i).matchData(uri)) {
+                return groups.get(i).getAction() == UriRelativeFilterGroup.ACTION_ALLOW;
+            }
+        }
+        return false;
+    }
+
+    /** @hide */
+    public static List<UriRelativeFilterGroup> parcelsToGroups(
+            @Nullable List<UriRelativeFilterGroupParcel> parcels) {
+        List<UriRelativeFilterGroup> groups = new ArrayList<>();
+        if (parcels != null) {
+            for (int i = 0; i < parcels.size(); i++) {
+                groups.add(new UriRelativeFilterGroup(parcels.get(i)));
+            }
+        }
+        return groups;
+    }
+
+    /** @hide */
+    public static List<UriRelativeFilterGroupParcel> groupsToParcels(
+            @Nullable List<UriRelativeFilterGroup> groups) {
+        List<UriRelativeFilterGroupParcel> parcels = new ArrayList<>();
+        if (groups != null) {
+            for (int i = 0; i < groups.size(); i++) {
+                parcels.add(groups.get(i).toParcel());
+            }
+        }
+        return parcels;
+    }
+
     /**
      * New UriRelativeFilterGroup that matches a Intent data.
      *
@@ -205,6 +242,35 @@
         }
     }
 
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        UriRelativeFilterGroup that = (UriRelativeFilterGroup) o;
+        if (mAction != that.mAction) return false;
+        return mUriRelativeFilters.equals(that.mUriRelativeFilters);
+    }
+
+    @Override
+    public int hashCode() {
+        int _hash = 0;
+        _hash = 31 * _hash + mAction;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mUriRelativeFilters);
+        return _hash;
+    }
+
+    /** @hide */
+    public UriRelativeFilterGroupParcel toParcel() {
+        UriRelativeFilterGroupParcel parcel = new UriRelativeFilterGroupParcel();
+        parcel.action = mAction;
+        parcel.filters = new ArrayList<>();
+        for (UriRelativeFilter filter : mUriRelativeFilters) {
+            parcel.filters.add(filter.toParcel());
+        }
+        return parcel;
+    }
+
     /** @hide */
     UriRelativeFilterGroup(@NonNull Parcel src) {
         mAction = src.readInt();
@@ -213,4 +279,12 @@
             mUriRelativeFilters.add(new UriRelativeFilter(src));
         }
     }
+
+    /** @hide */
+    public UriRelativeFilterGroup(UriRelativeFilterGroupParcel parcel) {
+        mAction = parcel.action;
+        for (int i = 0; i < parcel.filters.size(); i++) {
+            mUriRelativeFilters.add(new UriRelativeFilter(parcel.filters.get(i)));
+        }
+    }
 }
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/core/java/android/content/UriRelativeFilterGroupParcel.aidl
similarity index 71%
copy from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
copy to core/java/android/content/UriRelativeFilterGroupParcel.aidl
index 838e41e..3679e7f 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/core/java/android/content/UriRelativeFilterGroupParcel.aidl
@@ -14,11 +14,15 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package android.content;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
+import android.content.UriRelativeFilterParcel;
+
+/**
+ * Class for holding UriRelativeFilterGroup data.
+ * @hide
+ */
+parcelable UriRelativeFilterGroupParcel {
+    int action;
+    List<UriRelativeFilterParcel> filters;
 }
-
diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/core/java/android/content/UriRelativeFilterParcel.aidl
similarity index 64%
copy from core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
copy to core/java/android/content/UriRelativeFilterParcel.aidl
index 838e41e..4fb196d 100644
--- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl
+++ b/core/java/android/content/UriRelativeFilterParcel.aidl
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2024, 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.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      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,
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package android.hardware;
+package android.content;
 
-/** @hide */
-parcelable CameraPrivacyAllowlistEntry {
-    String packageName;
-    boolean isMandatory;
+/**
+ * Class for holding UriRelativeFilter data.
+ * @hide
+ */
+parcelable UriRelativeFilterParcel {
+    int uriPart;
+    int patternType;
+    String filter;
 }
-
diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
index 77bd147..4dcc517 100644
--- a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
+++ b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
@@ -17,6 +17,7 @@
 package android.content.pm.verify.domain;
 
 import android.annotation.CheckResult;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -25,15 +26,21 @@
 import android.annotation.SystemService;
 import android.content.Context;
 import android.content.Intent;
+import android.content.UriRelativeFilterGroup;
+import android.content.UriRelativeFilterGroupParcel;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.os.UserHandle;
+import android.util.ArrayMap;
 
 import com.android.internal.util.CollectionUtils;
 
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
@@ -156,6 +163,74 @@
     }
 
     /**
+     * Update the URI relative filter groups for a package. All previously existing groups
+     * will be cleared before the new groups will be applied.
+     *
+     * @param packageName The name of the package.
+     * @param domainToGroupsMap A map of domains to a list of {@link UriRelativeFilterGroup}s that
+     *                         should apply to them. Groups for each domain will replace any groups
+     *                         provided for that domain in a prior call to this method. Groups will
+     *                         be evaluated in the order they are provided.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT)
+    @FlaggedApi(android.content.pm.Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+    public void setUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(domainToGroupsMap);
+        Bundle bundle = new Bundle();
+        for (String domain : domainToGroupsMap.keySet()) {
+            List<UriRelativeFilterGroup> groups = domainToGroupsMap.get(domain);
+            bundle.putParcelableList(domain, UriRelativeFilterGroup.groupsToParcels(groups));
+        }
+        try {
+            mDomainVerificationManager.setUriRelativeFilterGroups(packageName, bundle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Retrieves a map of a package's verified domains to a list of {@link UriRelativeFilterGroup}s
+     * that applies to them.
+     *
+     * @param packageName The name of the package.
+     * @param domains List of domains for which to retrieve group matches.
+     * @return A map of domains to the lists of {@link UriRelativeFilterGroup}s that apply to them.
+     * @hide
+     */
+    @NonNull
+    @SystemApi
+    @FlaggedApi(android.content.pm.Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+    public Map<String, List<UriRelativeFilterGroup>> getUriRelativeFilterGroups(
+            @NonNull String packageName,
+            @NonNull List<String> domains) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(domains);
+        if (domains.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        try {
+            Bundle bundle = mDomainVerificationManager.getUriRelativeFilterGroups(packageName,
+                    domains);
+            ArrayMap<String, List<UriRelativeFilterGroup>> map = new ArrayMap<>();
+            if (!bundle.isEmpty()) {
+                for (String domain : bundle.keySet()) {
+                    List<UriRelativeFilterGroupParcel> parcels =
+                            bundle.getParcelableArrayList(domain,
+                                    UriRelativeFilterGroupParcel.class);
+                    map.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels));
+                }
+            }
+            return map;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Used to iterate all {@link DomainVerificationInfo} values to do cleanup or retries. This is
      * usually a heavy workload and should be done infrequently.
      *
diff --git a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
index 53205f3..f5af82d 100644
--- a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
+++ b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
@@ -20,6 +20,8 @@
 import android.content.pm.verify.domain.DomainSet;
 import android.content.pm.verify.domain.DomainVerificationInfo;
 import android.content.pm.verify.domain.DomainVerificationUserState;
+import android.content.UriRelativeFilterGroup;
+import android.os.Bundle;
 import java.util.List;
 
 /**
@@ -46,4 +48,8 @@
 
     int setDomainVerificationUserSelection(String domainSetId, in DomainSet domains,
             boolean enabled, int userId);
+
+    void setUriRelativeFilterGroups(String packageName, in Bundle domainToGroupsBundle);
+
+    Bundle getUriRelativeFilterGroups(String packageName, in List<String> domains);
 }
diff --git a/core/java/android/hardware/ISensorPrivacyListener.aidl b/core/java/android/hardware/ISensorPrivacyListener.aidl
index 19ae302..2ac21d2 100644
--- a/core/java/android/hardware/ISensorPrivacyListener.aidl
+++ b/core/java/android/hardware/ISensorPrivacyListener.aidl
@@ -25,6 +25,5 @@
     //   frameworks/native/libs/sensorprivacy/aidl/android/hardware/ISensorPrivacyListener.aidl
     // =============== Beginning of transactions used on native side as well ======================
     void onSensorPrivacyChanged(int toggleType, int sensor, boolean enabled);
-    void onSensorPrivacyStateChanged(int toggleType, int sensor, int state);
     // =============== End of transactions used on native side as well ============================
 }
diff --git a/core/java/android/hardware/ISensorPrivacyManager.aidl b/core/java/android/hardware/ISensorPrivacyManager.aidl
index 851ce2a..9cf329c 100644
--- a/core/java/android/hardware/ISensorPrivacyManager.aidl
+++ b/core/java/android/hardware/ISensorPrivacyManager.aidl
@@ -16,7 +16,6 @@
 
 package android.hardware;
 
-import android.hardware.CameraPrivacyAllowlistEntry;
 import android.hardware.ISensorPrivacyListener;
 
 /** @hide */
@@ -46,22 +45,6 @@
     void setToggleSensorPrivacy(int userId, int source, int sensor, boolean enable);
 
     void setToggleSensorPrivacyForProfileGroup(int userId, int source, int sensor, boolean enable);
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
-    List<CameraPrivacyAllowlistEntry> getCameraPrivacyAllowlist();
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
-    int getToggleSensorPrivacyState(int toggleType, int sensor);
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY)")
-    void setToggleSensorPrivacyState(int userId, int source, int sensor, int state);
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY)")
-    void setToggleSensorPrivacyStateForProfileGroup(int userId, int source, int sensor, int  state);
-
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)")
-    boolean isCameraPrivacyEnabled(String packageName);
-
     // =============== End of transactions used on native side as well ============================
 
     void suppressToggleSensorPrivacyReminders(int userId, int sensor, IBinder token,
@@ -70,4 +53,4 @@
     boolean requiresAuthentication();
 
     void showSensorUseDialog(int sensor);
-}
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java
index 4c0a4b9..18c95bfb 100644
--- a/core/java/android/hardware/SensorPrivacyManager.java
+++ b/core/java/android/hardware/SensorPrivacyManager.java
@@ -17,7 +17,6 @@
 package android.hardware;
 
 import android.Manifest;
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
@@ -39,11 +38,9 @@
 import android.util.Pair;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.camera.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -218,41 +215,13 @@
         public static final int DISABLED = SensorPrivacyIndividualEnabledSensorProto.DISABLED;
 
         /**
-         * Constant indicating privacy is enabled except for the automotive driver assistance apps
-         * which are helpful for driving.
-         */
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_HELPFUL_APPS;
-
-         /**
-         * Constant indicating privacy is enabled except for the automotive driver assistance apps
-         * which are required by car manufacturer for driving.
-         */
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_REQUIRED_APPS;
-
-        /**
-         * Constant indicating privacy is enabled except for the automotive driver assistance apps
-         * which are both helpful for driving and also apps required by car manufacturer for
-         * driving.
-         */
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_APPS =
-                SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_APPS;
-
-        /**
          * Types of state which can exist for a sensor privacy toggle
          *
          * @hide
          */
         @IntDef(value = {
                 ENABLED,
-                DISABLED,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS,
-                AUTOMOTIVE_DRIVER_ASSISTANCE_APPS
+                DISABLED
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface StateType {}
@@ -297,19 +266,6 @@
             private int mToggleType;
             private int mSensor;
             private boolean mEnabled;
-            private int mState;
-
-            @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-            private SensorPrivacyChangedParams(int toggleType, int sensor, int state) {
-                mToggleType = toggleType;
-                mSensor = sensor;
-                mState = state;
-                if (state == StateTypes.ENABLED) {
-                    mEnabled = true;
-                } else {
-                    mEnabled = false;
-                }
-            }
 
             private SensorPrivacyChangedParams(int toggleType, int sensor, boolean enabled) {
                 mToggleType = toggleType;
@@ -328,12 +284,6 @@
             public boolean isEnabled() {
                 return mEnabled;
             }
-
-            @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-            public @StateTypes.StateType int getState() {
-                return mState;
-            }
-
         }
     }
 
@@ -369,9 +319,6 @@
     private final ArrayMap<Pair<Integer, OnSensorPrivacyChangedListener>,
             OnSensorPrivacyChangedListener> mLegacyToggleListeners = new ArrayMap<>();
 
-    @GuardedBy("mLock")
-    private ArrayMap<String, Boolean> mCameraPrivacyAllowlist = null;
-
     /** The singleton ISensorPrivacyListener for IPC which will be used to dispatch to local
      * listeners */
     @NonNull
@@ -381,33 +328,12 @@
             synchronized (mLock) {
                 for (int i = 0; i < mToggleListeners.size(); i++) {
                     OnSensorPrivacyChangedListener listener = mToggleListeners.keyAt(i);
-                    if (Flags.privacyAllowlist()) {
-                        int state = enabled ?  StateTypes.ENABLED : StateTypes.DISABLED;
-                        mToggleListeners.valueAt(i).execute(() -> listener
-                                .onSensorPrivacyChanged(new OnSensorPrivacyChangedListener
-                                        .SensorPrivacyChangedParams(toggleType, sensor, state)));
-                    } else {
-                        mToggleListeners.valueAt(i).execute(() -> listener
-                                .onSensorPrivacyChanged(new OnSensorPrivacyChangedListener
-                                        .SensorPrivacyChangedParams(toggleType, sensor, enabled)));
-                    }
-                }
-            }
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public void onSensorPrivacyStateChanged(int toggleType, int sensor, int state) {
-            synchronized (mLock) {
-                for (int i = 0; i < mToggleListeners.size(); i++) {
-                    OnSensorPrivacyChangedListener listener = mToggleListeners.keyAt(i);
                     mToggleListeners.valueAt(i).execute(() -> listener
                             .onSensorPrivacyChanged(new OnSensorPrivacyChangedListener
-                                    .SensorPrivacyChangedParams(toggleType, sensor, state)));
+                                    .SensorPrivacyChangedParams(toggleType, sensor, enabled)));
                 }
             }
         }
-
     };
 
     /** Whether the singleton ISensorPrivacyListener has been registered */
@@ -723,73 +649,6 @@
     }
 
     /**
-     * Returns sensor privacy state for a specific sensor.
-     *
-     * @return int sensor privacy state.
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public @StateTypes.StateType int getSensorPrivacyState(@ToggleType int toggleType,
-            @Sensors.Sensor int sensor) {
-        try {
-            return mService.getToggleSensorPrivacyState(toggleType, sensor);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-  /**
-     * Returns if camera privacy is enabled for a specific package.
-     *
-     * @return boolean sensor privacy state.
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public boolean isCameraPrivacyEnabled(@NonNull String packageName) {
-        try {
-            return mService.isCameraPrivacyEnabled(packageName);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
-     * Returns camera privacy allowlist.
-     *
-     * @return List of automotive driver assistance packages for
-     * privacy allowlisting. The returned map includes the package
-     * name as key and the value is a Boolean which tells if that package
-     * is required by the car manufacturer as mandatory package for driving.
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public @NonNull Map<String, Boolean>  getCameraPrivacyAllowlist() {
-        synchronized (mLock) {
-            if (mCameraPrivacyAllowlist == null) {
-                mCameraPrivacyAllowlist = new ArrayMap<>();
-                try {
-                    for (CameraPrivacyAllowlistEntry entry :
-                            mService.getCameraPrivacyAllowlist()) {
-                        mCameraPrivacyAllowlist.put(entry.packageName, entry.isMandatory);
-                    }
-                } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
-                }
-            }
-            return mCameraPrivacyAllowlist;
-        }
-    }
-
-    /**
      * Sets sensor privacy to the specified state for an individual sensor.
      *
      * @param sensor the sensor which to change the state for
@@ -818,22 +677,6 @@
      * Sets sensor privacy to the specified state for an individual sensor.
      *
      * @param sensor the sensor which to change the state for
-     * @param state the state to which sensor privacy should be set.
-     *
-     * @hide
-     */
-    @SystemApi
-    @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public void setSensorPrivacyState(@Sensors.Sensor int sensor,
-            @StateTypes.StateType int state) {
-        setSensorPrivacyState(resolveSourceFromCurrentContext(), sensor, state);
-    }
-
-    /**
-     * Sets sensor privacy to the specified state for an individual sensor.
-     *
-     * @param sensor the sensor which to change the state for
      * @param enable the state to which sensor privacy should be set.
      *
      * @hide
@@ -865,27 +708,6 @@
     }
 
     /**
-     * Sets sensor privacy to the specified state for an individual sensor.
-     *
-     * @param sensor the sensor which to change the state for
-     * @param state the state to which sensor privacy should be set.
-     *
-     * @hide
-     */
-    @TestApi
-    @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public void setSensorPrivacyState(@Sources.Source int source, @Sensors.Sensor int sensor,
-            @StateTypes.StateType int state) {
-        try {
-            mService.setToggleSensorPrivacyState(mContext.getUserId(), source, sensor, state);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-
-    }
-
-    /**
      * Sets sensor privacy to the specified state for an individual sensor for the profile group of
      * context's user.
      *
@@ -923,28 +745,6 @@
     }
 
     /**
-     * Sets sensor privacy to the specified state for an individual sensor for the profile group of
-     * context's user.
-     *
-     * @param source the source using which the sensor is toggled.
-     * @param sensor the sensor which to change the state for
-     * @param state the state to which sensor privacy should be set.
-     *
-     * @hide
-     */
-    @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    public void setSensorPrivacyStateForProfileGroup(@Sources.Source int source,
-            @Sensors.Sensor int sensor, @StateTypes.StateType int state) {
-        try {
-            mService.setToggleSensorPrivacyStateForProfileGroup(mContext.getUserId(), source,
-                    sensor, state);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Don't show dialogs to turn off sensor privacy for this package.
      *
      * @param suppress Whether to suppress or re-enable.
@@ -1065,12 +865,6 @@
                             boolean enabled) {
                         listener.onAllSensorPrivacyChanged(enabled);
                     }
-
-                    @Override
-                    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-                    public void onSensorPrivacyStateChanged(int toggleType, int sensor,
-                            int state) {
-                    }
                 };
                 mListeners.put(listener, iListener);
             }
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 7abe821..3b10e0d 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -19,7 +19,9 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
@@ -40,6 +42,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemProperties;
+import android.provider.Settings;
 import android.util.IntArray;
 import android.util.Log;
 import android.util.Pair;
@@ -53,6 +56,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -243,16 +247,19 @@
         private static final String PROXY_SERVICE_NAME =
                 "com.android.cameraextensions.CameraExtensionsProxyService";
 
+        @FlaggedApi(Flags.FLAG_CONCERT_MODE)
+        private static final int FALLBACK_PACKAGE_NAME =
+                com.android.internal.R.string.config_extensionFallbackPackageName;
+        @FlaggedApi(Flags.FLAG_CONCERT_MODE)
+        private static final int FALLBACK_SERVICE_NAME =
+                com.android.internal.R.string.config_extensionFallbackServiceName;
+
         // Singleton instance
         private static final CameraExtensionManagerGlobal GLOBAL_CAMERA_MANAGER =
                 new CameraExtensionManagerGlobal();
         private final Object mLock = new Object();
         private final int PROXY_SERVICE_DELAY_MS = 2000;
-        private InitializerFuture mInitFuture = null;
-        private ServiceConnection mConnection = null;
-        private int mConnectionCount = 0;
-        private ICameraExtensionsProxyService mProxy = null;
-        private boolean mSupportsAdvancedExtensions = false;
+        private ExtensionConnectionManager mConnectionManager = new ExtensionConnectionManager();
 
         // Singleton, don't allow construction
         private CameraExtensionManagerGlobal() {}
@@ -261,17 +268,17 @@
             return GLOBAL_CAMERA_MANAGER;
         }
 
-        private void releaseProxyConnectionLocked(Context ctx) {
-            if (mConnection != null ) {
-                ctx.unbindService(mConnection);
-                mConnection = null;
-                mProxy = null;
-                mConnectionCount = 0;
+        private void releaseProxyConnectionLocked(Context ctx, int extension) {
+            if (mConnectionManager.getConnection(extension) != null) {
+                ctx.unbindService(mConnectionManager.getConnection(extension));
+                mConnectionManager.setConnection(extension, null);
+                mConnectionManager.setProxy(extension, null);
+                mConnectionManager.resetConnectionCount(extension);
             }
         }
 
-        private void connectToProxyLocked(Context ctx) {
-            if (mConnection == null) {
+        private void connectToProxyLocked(Context ctx, int extension, boolean useFallback) {
+            if (mConnectionManager.getConnection(extension) == null) {
                 Intent intent = new Intent();
                 intent.setClassName(PROXY_PACKAGE_NAME, PROXY_SERVICE_NAME);
                 String vendorProxyPackage = SystemProperties.get(
@@ -287,34 +294,55 @@
                       + vendorProxyService);
                   intent.setClassName(vendorProxyPackage, vendorProxyService);
                 }
-                mInitFuture = new InitializerFuture();
-                mConnection = new ServiceConnection() {
+
+                if (Flags.concertMode() && useFallback) {
+                    String packageName = ctx.getResources().getString(FALLBACK_PACKAGE_NAME);
+                    String serviceName = ctx.getResources().getString(FALLBACK_SERVICE_NAME);
+
+                    if (!packageName.isEmpty() && !serviceName.isEmpty()) {
+                        Log.v(TAG,
+                                "Choosing the fallback software implementation package: "
+                                + packageName);
+                        Log.v(TAG,
+                                "Choosing the fallback software implementation service: "
+                                + serviceName);
+                        intent.setClassName(packageName, serviceName);
+                    }
+                }
+
+                InitializerFuture initFuture = new InitializerFuture();
+                ServiceConnection connection = new ServiceConnection() {
                     @Override
                     public void onServiceDisconnected(ComponentName component) {
-                        mConnection = null;
-                        mProxy = null;
+                        mConnectionManager.setConnection(extension, null);
+                        mConnectionManager.setProxy(extension, null);
                     }
 
                     @Override
                     public void onServiceConnected(ComponentName component, IBinder binder) {
-                        mProxy = ICameraExtensionsProxyService.Stub.asInterface(binder);
-                        if (mProxy == null) {
+                        ICameraExtensionsProxyService proxy =
+                                ICameraExtensionsProxyService.Stub.asInterface(binder);
+                        mConnectionManager.setProxy(extension, proxy);
+                        if (mConnectionManager.getProxy(extension) == null) {
                             throw new IllegalStateException("Camera Proxy service is null");
                         }
                         try {
-                            mSupportsAdvancedExtensions = mProxy.advancedExtensionsSupported();
+                            mConnectionManager.setAdvancedExtensionsSupported(extension,
+                                    mConnectionManager.getProxy(extension)
+                                    .advancedExtensionsSupported());
                         } catch (RemoteException e) {
                             Log.e(TAG, "Remote IPC failed!");
                         }
-                        mInitFuture.setStatus(true);
+                        initFuture.setStatus(true);
                     }
                 };
                 ctx.bindService(intent, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT |
                         Context.BIND_ABOVE_CLIENT | Context.BIND_NOT_VISIBLE,
-                        android.os.AsyncTask.THREAD_POOL_EXECUTOR, mConnection);
+                        android.os.AsyncTask.THREAD_POOL_EXECUTOR, connection);
+                mConnectionManager.setConnection(extension, connection);
 
                 try {
-                    mInitFuture.get(PROXY_SERVICE_DELAY_MS, TimeUnit.MILLISECONDS);
+                    initFuture.get(PROXY_SERVICE_DELAY_MS, TimeUnit.MILLISECONDS);
                 } catch (TimeoutException e) {
                     Log.e(TAG, "Timed out while initializing proxy service!");
                 }
@@ -366,64 +394,102 @@
             }
         }
 
-        public boolean registerClient(Context ctx, IBinder token) {
+        public boolean registerClientHelper(Context ctx, IBinder token, int extension,
+                boolean useFallback) {
             synchronized (mLock) {
                 boolean ret = false;
-                connectToProxyLocked(ctx);
-                if (mProxy == null) {
+                connectToProxyLocked(ctx, extension, useFallback);
+                if (mConnectionManager.getProxy(extension) == null) {
                     return false;
                 }
-                mConnectionCount++;
+                mConnectionManager.incrementConnectionCount(extension);
 
                 try {
-                    ret = mProxy.registerClient(token);
+                    ret = mConnectionManager.getProxy(extension).registerClient(token);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to initialize extension! Extension service does "
                             + " not respond!");
                 }
                 if (!ret) {
-                    mConnectionCount--;
+                    mConnectionManager.decrementConnectionCount(extension);
                 }
 
-                if (mConnectionCount <= 0) {
-                    releaseProxyConnectionLocked(ctx);
+                if (mConnectionManager.getConnectionCount(extension) <= 0) {
+                    releaseProxyConnectionLocked(ctx, extension);
                 }
 
                 return ret;
             }
         }
 
-        public void unregisterClient(Context ctx, IBinder token) {
+        @SuppressLint("NonUserGetterCalled")
+        public boolean registerClient(Context ctx, IBinder token, int extension,
+                String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) {
+            boolean ret = registerClientHelper(ctx, token, extension, false /*useFallback*/);
+
+            if (Flags.concertMode()) {
+                // Check if user enabled fallback impl
+                ContentResolver resolver = ctx.getContentResolver();
+                int userEnabled = Settings.Secure.getInt(resolver,
+                        Settings.Secure.CAMERA_EXTENSIONS_FALLBACK, 1);
+
+                boolean vendorImpl = true;
+                if (ret && (mConnectionManager.getProxy(extension) != null) && (userEnabled == 1)) {
+                    // At this point, we are connected to either CameraExtensionsProxyService or
+                    // the vendor extension proxy service. If the vendor does not support the
+                    // extension, unregisterClient and re-register client with the proxy service
+                    // containing the fallback impl
+                    vendorImpl = isExtensionSupported(cameraId, extension,
+                            characteristicsMapNative);
+                }
+
+                if (!vendorImpl) {
+                    unregisterClient(ctx, token, extension);
+                    ret = registerClientHelper(ctx, token, extension, true /*useFallback*/);
+
+                }
+            }
+
+            return ret;
+        }
+
+        public void unregisterClient(Context ctx, IBinder token, int extension) {
             synchronized (mLock) {
-                if (mProxy != null) {
+                if (mConnectionManager.getProxy(extension) != null) {
                     try {
-                        mProxy.unregisterClient(token);
+                        mConnectionManager.getProxy(extension).unregisterClient(token);
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to de-initialize extension! Extension service does"
                                 + " not respond!");
                     } finally {
-                        mConnectionCount--;
-                        if (mConnectionCount <= 0) {
-                            releaseProxyConnectionLocked(ctx);
+                        mConnectionManager.decrementConnectionCount(extension);
+                        if (mConnectionManager.getConnectionCount(extension) <= 0) {
+                            releaseProxyConnectionLocked(ctx, extension);
                         }
                     }
                 }
             }
         }
 
-        public void initializeSession(IInitializeSessionCallback cb) throws RemoteException {
+        public void initializeSession(IInitializeSessionCallback cb, int extension)
+                throws RemoteException {
             synchronized (mLock) {
-                if (mProxy != null) {
-                    mProxy.initializeSession(cb);
+                if (mConnectionManager.getProxy(extension) != null
+                        && !mConnectionManager.isSessionInitialized()) {
+                    mConnectionManager.getProxy(extension).initializeSession(cb);
+                    mConnectionManager.setSessionInitialized(true);
+                } else {
+                    cb.onFailure();
                 }
             }
         }
 
-        public void releaseSession() {
+        public void releaseSession(int extension) {
             synchronized (mLock) {
-                if (mProxy != null) {
+                if (mConnectionManager.getProxy(extension) != null) {
                     try {
-                        mProxy.releaseSession();
+                        mConnectionManager.getProxy(extension).releaseSession();
+                        mConnectionManager.setSessionInitialized(false);
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to release session! Extension service does"
                                 + " not respond!");
@@ -432,77 +498,157 @@
             }
         }
 
-        public boolean areAdvancedExtensionsSupported() {
-            return mSupportsAdvancedExtensions;
+        public boolean areAdvancedExtensionsSupported(int extension) {
+            return mConnectionManager.areAdvancedExtensionsSupported(extension);
         }
 
-        public IPreviewExtenderImpl initializePreviewExtension(int extensionType)
+        public IPreviewExtenderImpl initializePreviewExtension(int extension)
                 throws RemoteException {
             synchronized (mLock) {
-                if (mProxy != null) {
-                    return mProxy.initializePreviewExtension(extensionType);
+                if (mConnectionManager.getProxy(extension) != null) {
+                    return mConnectionManager.getProxy(extension)
+                            .initializePreviewExtension(extension);
                 } else {
                     return null;
                 }
             }
         }
 
-        public IImageCaptureExtenderImpl initializeImageExtension(int extensionType)
+        public IImageCaptureExtenderImpl initializeImageExtension(int extension)
                 throws RemoteException {
             synchronized (mLock) {
-                if (mProxy != null) {
-                    return mProxy.initializeImageExtension(extensionType);
+                if (mConnectionManager.getProxy(extension) != null) {
+                    return mConnectionManager.getProxy(extension)
+                            .initializeImageExtension(extension);
                 } else {
                     return null;
                 }
             }
         }
 
-        public IAdvancedExtenderImpl initializeAdvancedExtension(int extensionType)
+        public IAdvancedExtenderImpl initializeAdvancedExtension(int extension)
                 throws RemoteException {
             synchronized (mLock) {
-                if (mProxy != null) {
-                    return mProxy.initializeAdvancedExtension(extensionType);
+                if (mConnectionManager.getProxy(extension) != null) {
+                    return mConnectionManager.getProxy(extension)
+                            .initializeAdvancedExtension(extension);
                 } else {
                     return null;
                 }
             }
         }
+
+        private class ExtensionConnectionManager {
+            // Maps extension to ExtensionConnection
+            private Map<Integer, ExtensionConnection> mConnections = new HashMap<>();
+            private boolean mSessionInitialized = false;
+
+            public ExtensionConnectionManager() {
+                IntArray extensionList = new IntArray(EXTENSION_LIST.length);
+                extensionList.addAll(EXTENSION_LIST);
+                if (Flags.concertMode()) {
+                    extensionList.add(EXTENSION_EYES_FREE_VIDEOGRAPHY);
+                }
+
+                for (int extensionType : extensionList.toArray()) {
+                    mConnections.put(extensionType, new ExtensionConnection());
+                }
+            }
+
+            public ICameraExtensionsProxyService getProxy(@Extension int extension) {
+                return mConnections.get(extension).mProxy;
+            }
+
+            public ServiceConnection getConnection(@Extension int extension) {
+                return mConnections.get(extension).mConnection;
+            }
+
+            public int getConnectionCount(@Extension int extension) {
+                return mConnections.get(extension).mConnectionCount;
+            }
+
+            public boolean areAdvancedExtensionsSupported(@Extension int extension) {
+                return mConnections.get(extension).mSupportsAdvancedExtensions;
+            }
+
+            public boolean isSessionInitialized() {
+                return mSessionInitialized;
+            }
+
+            public void setProxy(@Extension int extension, ICameraExtensionsProxyService proxy) {
+                mConnections.get(extension).mProxy = proxy;
+            }
+
+            public void setConnection(@Extension int extension, ServiceConnection connection) {
+                mConnections.get(extension).mConnection = connection;
+            }
+
+            public void incrementConnectionCount(@Extension int extension) {
+                mConnections.get(extension).mConnectionCount++;
+            }
+
+            public void decrementConnectionCount(@Extension int extension) {
+                mConnections.get(extension).mConnectionCount--;
+            }
+
+            public void resetConnectionCount(@Extension int extension) {
+                mConnections.get(extension).mConnectionCount = 0;
+            }
+
+            public void setAdvancedExtensionsSupported(@Extension int extension,
+                    boolean advancedExtSupported) {
+                mConnections.get(extension).mSupportsAdvancedExtensions = advancedExtSupported;
+            }
+
+            public void setSessionInitialized(boolean initialized) {
+                mSessionInitialized = initialized;
+            }
+
+            private class ExtensionConnection {
+                public ICameraExtensionsProxyService mProxy = null;
+                public ServiceConnection mConnection = null;
+                public int mConnectionCount = 0;
+                public boolean mSupportsAdvancedExtensions = false;
+            }
+        }
     }
 
     /**
      * @hide
      */
-    public static boolean registerClient(Context ctx, IBinder token) {
-        return CameraExtensionManagerGlobal.get().registerClient(ctx, token);
+    public static boolean registerClient(Context ctx, IBinder token, int extension,
+            String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) {
+        return CameraExtensionManagerGlobal.get().registerClient(ctx, token, extension, cameraId,
+                characteristicsMapNative);
     }
 
     /**
      * @hide
      */
-    public static void unregisterClient(Context ctx, IBinder token) {
-        CameraExtensionManagerGlobal.get().unregisterClient(ctx, token);
+    public static void unregisterClient(Context ctx, IBinder token, int extension) {
+        CameraExtensionManagerGlobal.get().unregisterClient(ctx, token, extension);
     }
 
     /**
      * @hide
      */
-    public static void initializeSession(IInitializeSessionCallback cb) throws RemoteException {
-        CameraExtensionManagerGlobal.get().initializeSession(cb);
+    public static void initializeSession(IInitializeSessionCallback cb, int extension)
+            throws RemoteException {
+        CameraExtensionManagerGlobal.get().initializeSession(cb, extension);
     }
 
     /**
      * @hide
      */
-    public static void releaseSession() {
-        CameraExtensionManagerGlobal.get().releaseSession();
+    public static void releaseSession(int extension) {
+        CameraExtensionManagerGlobal.get().releaseSession(extension);
     }
 
     /**
      * @hide
      */
-    public static boolean areAdvancedExtensionsSupported() {
-        return CameraExtensionManagerGlobal.get().areAdvancedExtensionsSupported();
+    public static boolean areAdvancedExtensionsSupported(int extension) {
+        return CameraExtensionManagerGlobal.get().areAdvancedExtensionsSupported(extension);
     }
 
     /**
@@ -510,7 +656,7 @@
      */
     public static boolean isExtensionSupported(String cameraId, int extensionType,
             Map<String, CameraMetadataNative> characteristicsMap) {
-        if (areAdvancedExtensionsSupported()) {
+        if (areAdvancedExtensionsSupported(extensionType)) {
             try {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extensionType);
                 return extender.isExtensionAvailable(cameraId, characteristicsMap);
@@ -600,24 +746,24 @@
     public @NonNull List<Integer> getSupportedExtensions() {
         ArrayList<Integer> ret = new ArrayList<>();
         final IBinder token = new Binder(TAG + "#getSupportedExtensions:" + mCameraId);
-        boolean success = registerClient(mContext, token);
-        if (!success) {
-            return Collections.unmodifiableList(ret);
-        }
 
         IntArray extensionList = new IntArray(EXTENSION_LIST.length);
         extensionList.addAll(EXTENSION_LIST);
         if (Flags.concertMode()) {
             extensionList.add(EXTENSION_EYES_FREE_VIDEOGRAPHY);
         }
-        try {
-            for (int extensionType : extensionList.toArray()) {
-                if (isExtensionSupported(mCameraId, extensionType, mCharacteristicsMapNative)) {
+
+        for (int extensionType : extensionList.toArray()) {
+            try {
+                boolean success = registerClient(mContext, token, extensionType, mCameraId,
+                        mCharacteristicsMapNative);
+                if (success && isExtensionSupported(mCameraId, extensionType,
+                        mCharacteristicsMapNative)) {
                     ret.add(extensionType);
                 }
+            } finally {
+                unregisterClient(mContext, token, extensionType);
             }
-        } finally {
-            unregisterClient(mContext, token);
         }
 
         return Collections.unmodifiableList(ret);
@@ -643,7 +789,8 @@
     public <T> @Nullable T get(@Extension int extension,
             @NonNull CameraCharacteristics.Key<T> key) {
         final IBinder token = new Binder(TAG + "#get:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -653,7 +800,7 @@
                 throw new IllegalArgumentException("Unsupported extension");
             }
 
-            if (areAdvancedExtensionsSupported() && getKeys(extension).contains(key)) {
+            if (areAdvancedExtensionsSupported(extension) && getKeys(extension).contains(key)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 CameraMetadataNative metadata =
@@ -670,7 +817,7 @@
             Log.e(TAG, "Failed to query the extension for the specified key! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
         return null;
     }
@@ -691,7 +838,8 @@
     public @NonNull Set<CameraCharacteristics.Key> getKeys(@Extension int extension) {
         final IBinder token =
                 new Binder(TAG + "#getKeys:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -703,7 +851,7 @@
                 throw new IllegalArgumentException("Unsupported extension");
             }
 
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 CameraMetadataNative metadata =
@@ -732,7 +880,7 @@
             Log.e(TAG, "Failed to query the extension for all available keys! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
         return Collections.unmodifiableSet(ret);
     }
@@ -755,7 +903,8 @@
      */
     public boolean isPostviewAvailable(@Extension int extension) {
         final IBinder token = new Binder(TAG + "#isPostviewAvailable:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -765,7 +914,7 @@
                 throw new IllegalArgumentException("Unsupported extension");
             }
 
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 return extender.isPostviewAvailable();
@@ -779,7 +928,7 @@
             Log.e(TAG, "Failed to query the extension for postview availability! Extension "
                     + "service does not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return false;
@@ -813,7 +962,8 @@
     public List<Size> getPostviewSupportedSizes(@Extension int extension,
             @NonNull Size captureSize, int format) {
         final IBinder token = new Binder(TAG + "#getPostviewSupportedSizes:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -831,7 +981,7 @@
             StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
                     CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
 
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 switch(format) {
                     case ImageFormat.YUV_420_888:
                     case ImageFormat.JPEG:
@@ -879,7 +1029,7 @@
                     + "service does not respond!");
             return Collections.emptyList();
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
     }
 
@@ -917,7 +1067,8 @@
         //       ambiguity is resolved in b/169799538.
 
         final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -929,7 +1080,7 @@
 
             StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
                     CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 return generateSupportedSizes(
@@ -948,7 +1099,7 @@
                     + " not respond!");
             return new ArrayList<>();
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
     }
 
@@ -978,7 +1129,8 @@
     List<Size> getExtensionSupportedSizes(@Extension int extension, int format) {
         try {
             final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId);
-            boolean success = registerClient(mContext, token);
+            boolean success = registerClient(mContext, token, extension, mCameraId,
+                    mCharacteristicsMapNative);
             if (!success) {
                 throw new IllegalArgumentException("Unsupported extensions");
             }
@@ -990,7 +1142,7 @@
 
                 StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
                         CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
-                if (areAdvancedExtensionsSupported()) {
+                if (areAdvancedExtensionsSupported(extension)) {
                     switch(format) {
                         case ImageFormat.YUV_420_888:
                         case ImageFormat.JPEG:
@@ -1035,7 +1187,7 @@
                     }
                 }
             } finally {
-                unregisterClient(mContext, token);
+                unregisterClient(mContext, token, extension);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to query the extension supported sizes! Extension service does"
@@ -1073,7 +1225,8 @@
         }
 
         final IBinder token = new Binder(TAG + "#getEstimatedCaptureLatencyRangeMillis:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -1087,7 +1240,7 @@
                     new android.hardware.camera2.extension.Size();
             sz.width = captureOutputSize.getWidth();
             sz.height = captureOutputSize.getHeight();
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 LatencyRange latencyRange = extender.getEstimatedCaptureLatencyRange(mCameraId,
@@ -1126,7 +1279,7 @@
             Log.e(TAG, "Failed to query the extension capture latency! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return null;
@@ -1143,7 +1296,8 @@
      */
     public boolean isCaptureProcessProgressAvailable(@Extension int extension) {
         final IBinder token = new Binder(TAG + "#isCaptureProcessProgressAvailable:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -1153,7 +1307,7 @@
                 throw new IllegalArgumentException("Unsupported extension");
             }
 
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 return extender.isCaptureProcessProgressAvailable();
@@ -1167,7 +1321,7 @@
             Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does"
                     + " not respond!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return false;
@@ -1195,7 +1349,8 @@
     @NonNull
     public Set<CaptureRequest.Key> getAvailableCaptureRequestKeys(@Extension int extension) {
         final IBinder token = new Binder(TAG + "#getAvailableCaptureRequestKeys:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -1208,7 +1363,7 @@
             }
 
             CameraMetadataNative captureRequestMeta = null;
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 captureRequestMeta = extender.getAvailableCaptureRequestKeys(mCameraId);
@@ -1250,7 +1405,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture request keys!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return Collections.unmodifiableSet(ret);
@@ -1282,7 +1437,8 @@
     @NonNull
     public Set<CaptureResult.Key> getAvailableCaptureResultKeys(@Extension int extension) {
         final IBinder token = new Binder(TAG + "#getAvailableCaptureResultKeys:" + mCameraId);
-        boolean success = registerClient(mContext, token);
+        boolean success = registerClient(mContext, token, extension, mCameraId,
+                mCharacteristicsMapNative);
         if (!success) {
             throw new IllegalArgumentException("Unsupported extensions");
         }
@@ -1294,7 +1450,7 @@
             }
 
             CameraMetadataNative captureResultMeta = null;
-            if (areAdvancedExtensionsSupported()) {
+            if (areAdvancedExtensionsSupported(extension)) {
                 IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
                 extender.init(mCameraId, mCharacteristicsMapNative);
                 captureResultMeta = extender.getAvailableCaptureResultKeys(mCameraId);
@@ -1336,7 +1492,7 @@
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to query the available capture result keys!");
         } finally {
-            unregisterClient(mContext, token);
+            unregisterClient(mContext, token, extension);
         }
 
         return Collections.unmodifiableSet(ret);
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index f6c8f36..b2032fa 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -102,6 +102,8 @@
 
     private boolean mInitialized;
     private boolean mSessionClosed;
+    private int mExtensionType;
+
 
     private final Context mContext;
 
@@ -205,7 +207,7 @@
         CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx,
                 extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface,
                 burstCaptureSurface, postviewSurface, config.getStateCallback(),
-                config.getExecutor(), sessionId, token);
+                config.getExecutor(), sessionId, token, config.getExtension());
 
         ret.mStatsAggregator.setClientName(ctx.getOpPackageName());
         ret.mStatsAggregator.setExtensionType(config.getExtension());
@@ -223,7 +225,8 @@
             @Nullable Surface postviewSurface,
             @NonNull StateCallback callback, @NonNull Executor executor,
             int sessionId,
-            @NonNull IBinder token) {
+            @NonNull IBinder token,
+            int extension) {
         mContext = ctx;
         mAdvancedExtender = extender;
         mCameraDevice = cameraDevice;
@@ -242,6 +245,7 @@
         mSessionId = sessionId;
         mToken = token;
         mInterfaceLock = cameraDevice.mInterfaceLock;
+        mExtensionType = extension;
 
         mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(),
                 /*isAdvanced=*/true);
@@ -583,9 +587,9 @@
             if (mToken != null) {
                 if (mInitialized || (mCaptureSession != null)) {
                     notifyClose = true;
-                    CameraExtensionCharacteristics.releaseSession();
+                    CameraExtensionCharacteristics.releaseSession(mExtensionType);
                 }
-                CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
+                CameraExtensionCharacteristics.unregisterClient(mContext, mToken, mExtensionType);
             }
             mInitialized = false;
             mToken = null;
@@ -654,7 +658,8 @@
             }
 
             try {
-                CameraExtensionCharacteristics.initializeSession(mInitializeHandler);
+                CameraExtensionCharacteristics.initializeSession(
+                        mInitializeHandler, mExtensionType);
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to initialize session! Extension service does"
                         + " not respond!");
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index ccb24e7..f03876b 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -2559,13 +2559,16 @@
         boolean initializationFailed = true;
         IBinder token = new Binder(TAG + " : " + mNextSessionId++);
         try {
-            boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token);
+            boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token,
+                    extensionConfiguration.getExtension(), mCameraId,
+                    CameraExtensionUtils.getCharacteristicsMapNative(characteristicsMap));
             if (!ret) {
                 token = null;
                 throw new UnsupportedOperationException("Unsupported extension!");
             }
 
-            if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported()) {
+            if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported(
+                        extensionConfiguration.getExtension())) {
                 mCurrentAdvancedExtensionSession =
                         CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession(
                                 this, characteristicsMap, mContext, extensionConfiguration,
@@ -2580,7 +2583,8 @@
             throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
         } finally {
             if (initializationFailed && (token != null)) {
-                CameraExtensionCharacteristics.unregisterClient(mContext, token);
+                CameraExtensionCharacteristics.unregisterClient(mContext, token,
+                        extensionConfiguration.getExtension());
             }
         }
     }
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index db7055b..725b413 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -118,6 +118,7 @@
     // In case the client doesn't explicitly enable repeating requests, the framework
     // will do so internally.
     private boolean mInternalRepeatingRequestEnabled = true;
+    private int mExtensionType;
 
     private final Context mContext;
 
@@ -244,7 +245,8 @@
                 sessionId,
                 token,
                 extensionChars.getAvailableCaptureRequestKeys(config.getExtension()),
-                extensionChars.getAvailableCaptureResultKeys(config.getExtension()));
+                extensionChars.getAvailableCaptureResultKeys(config.getExtension()),
+                config.getExtension());
 
         session.mStatsAggregator.setClientName(ctx.getOpPackageName());
         session.mStatsAggregator.setExtensionType(config.getExtension());
@@ -266,7 +268,8 @@
             int sessionId,
             @NonNull IBinder token,
             @NonNull Set<CaptureRequest.Key> requestKeys,
-            @Nullable Set<CaptureResult.Key> resultKeys) {
+            @Nullable Set<CaptureResult.Key> resultKeys,
+            int extension) {
         mContext = ctx;
         mImageExtender = imageExtender;
         mPreviewExtender = previewExtender;
@@ -289,6 +292,7 @@
         mSupportedResultKeys = resultKeys;
         mCaptureResultsSupported = !resultKeys.isEmpty();
         mInterfaceLock = cameraDevice.mInterfaceLock;
+        mExtensionType = extension;
 
         mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(),
                 /*isAdvanced=*/false);
@@ -881,9 +885,9 @@
             if (mToken != null) {
                 if (mInitialized || (mCaptureSession != null)) {
                     notifyClose = true;
-                    CameraExtensionCharacteristics.releaseSession();
+                    CameraExtensionCharacteristics.releaseSession(mExtensionType);
                 }
-                CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
+                CameraExtensionCharacteristics.unregisterClient(mContext, mToken, mExtensionType);
             }
             mInitialized = false;
             mToken = null;
@@ -1000,7 +1004,8 @@
                 mStatsAggregator.commit(/*isFinal*/false);
                 try {
                     finishPipelineInitialization();
-                    CameraExtensionCharacteristics.initializeSession(mInitializeHandler);
+                    CameraExtensionCharacteristics.initializeSession(
+                            mInitializeHandler, mExtensionType);
                 } catch (RemoteException e) {
                     Log.e(TAG, "Failed to initialize session! Extension service does"
                             + " not respond!");
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index b68b94d..a6b2ed0 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -31,6 +31,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.location.GnssSignalQuality;
+import android.net.NetworkCapabilities;
 import android.os.BatteryStatsManager.WifiState;
 import android.os.BatteryStatsManager.WifiSupplState;
 import android.server.ServerProtoEnums;
@@ -59,6 +60,7 @@
 import com.android.internal.os.CpuScalingPolicies;
 import com.android.internal.os.MonotonicClock;
 import com.android.internal.os.PowerStats;
+import com.android.net.module.util.NetworkCapabilitiesUtils;
 
 import com.google.android.collect.Lists;
 
@@ -2734,26 +2736,28 @@
         "emngcy", "other"
     };
 
+    public static final int NUM_ALL_NETWORK_TYPES = getAllNetworkTypesCount();
     public static final int DATA_CONNECTION_OUT_OF_SERVICE = 0;
-    public static final int DATA_CONNECTION_EMERGENCY_SERVICE = getEmergencyNetworkConnectionType();
-    public static final int DATA_CONNECTION_OTHER = DATA_CONNECTION_EMERGENCY_SERVICE + 1;
+    public static final int DATA_CONNECTION_EMERGENCY_SERVICE = NUM_ALL_NETWORK_TYPES + 1;
+    public static final int DATA_CONNECTION_OTHER = NUM_ALL_NETWORK_TYPES + 2;
 
     @UnsupportedAppUsage
-    public static final int NUM_DATA_CONNECTION_TYPES = DATA_CONNECTION_OTHER + 1;
+    public static final int NUM_DATA_CONNECTION_TYPES = NUM_ALL_NETWORK_TYPES + 3;
+
 
     @android.ravenwood.annotation.RavenwoodReplace
-    private static int getEmergencyNetworkConnectionType() {
+    public static int getAllNetworkTypesCount() {
         int count = TelephonyManager.getAllNetworkTypes().length;
         if (DATA_CONNECTION_NAMES.length != count + 3) {        // oos, emngcy, other
             throw new IllegalStateException(
                     "DATA_CONNECTION_NAMES length does not match network type count. "
                     + "Expected: " + (count + 3) + ", actual:" + DATA_CONNECTION_NAMES.length);
         }
-        return count + 1;
+        return count;
     }
 
-    private static int getEmergencyNetworkConnectionType$ravenwood() {
-        return DATA_CONNECTION_NAMES.length - 2;
+    public static int getAllNetworkTypesCount$ravenwood() {
+        return DATA_CONNECTION_NAMES.length - 3;  // oos, emngcy, other
     }
 
     /**
@@ -9071,4 +9075,31 @@
     protected static boolean isKernelStatsAvailable$ravenwood() {
         return false;
     }
+
+    @android.ravenwood.annotation.RavenwoodReplace
+    protected static int getDisplayTransport(int[] transports) {
+        return NetworkCapabilitiesUtils.getDisplayTransport(transports);
+    }
+
+    // See NetworkCapabilitiesUtils
+    private static final int[] DISPLAY_TRANSPORT_PRIORITIES = new int[] {
+            NetworkCapabilities.TRANSPORT_VPN,
+            NetworkCapabilities.TRANSPORT_CELLULAR,
+            NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+            NetworkCapabilities.TRANSPORT_BLUETOOTH,
+            NetworkCapabilities.TRANSPORT_WIFI,
+            NetworkCapabilities.TRANSPORT_ETHERNET,
+            NetworkCapabilities.TRANSPORT_USB
+    };
+
+    protected static int getDisplayTransport$ravenwood(int[] transports) {
+        for (int transport : DISPLAY_TRANSPORT_PRIORITIES) {
+            for (int t : transports) {
+                if (t == transport) {
+                    return transport;
+                }
+            }
+        }
+        return transports[0];
+    }
 }
diff --git a/core/java/android/os/BluetoothBatteryStats.java b/core/java/android/os/BluetoothBatteryStats.java
index 3d99a08..fa8f39d 100644
--- a/core/java/android/os/BluetoothBatteryStats.java
+++ b/core/java/android/os/BluetoothBatteryStats.java
@@ -26,6 +26,7 @@
  *
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class BluetoothBatteryStats implements Parcelable {
 
     /** @hide */
diff --git a/core/java/android/os/UserBatteryConsumer.java b/core/java/android/os/UserBatteryConsumer.java
index a2ff078..23ba0c6 100644
--- a/core/java/android/os/UserBatteryConsumer.java
+++ b/core/java/android/os/UserBatteryConsumer.java
@@ -34,6 +34,7 @@
  *
  * {@hide}
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class UserBatteryConsumer extends BatteryConsumer {
     static final int CONSUMER_TYPE_USER = 2;
 
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 0fbdbc4..ce03798 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -180,11 +180,14 @@
 
 
     /**
-     * User type representing a private profile.
+     * User type representing a private profile. Private profile is a user profile that can be used
+     * as an alternative user-space to install and use sensitive apps.
+     * UI surfaces can adopt an alternative strategy to show apps belonging to this profile, in line
+     * with their sensitive nature.
      * @hide
      */
     @FlaggedApi(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE)
-    @TestApi
+    @SystemApi
     public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE";
 
     /**
diff --git a/core/java/android/os/WakeLockStats.java b/core/java/android/os/WakeLockStats.java
index 05a7313..69e70a0 100644
--- a/core/java/android/os/WakeLockStats.java
+++ b/core/java/android/os/WakeLockStats.java
@@ -25,6 +25,7 @@
  * Snapshot of wake lock stats.
  *  @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class WakeLockStats implements Parcelable {
 
     /** @hide */
diff --git a/core/java/android/provider/ContactKeysManager.java b/core/java/android/provider/ContactKeysManager.java
index bef6456..01aaa3d 100644
--- a/core/java/android/provider/ContactKeysManager.java
+++ b/core/java/android/provider/ContactKeysManager.java
@@ -39,18 +39,19 @@
 import java.util.Objects;
 
 /**
- * ContactKeysManager provides the access to the E2EE contact keys provider.
- * It manages two types of keys - {@link ContactKey} of other users' and the owner's keys -
- * {@link SelfKey}.
+ * ContactKeysManager provides access to the provider of end-to-end encryption contact keys.
+ * It manages two types of keys - {@link ContactKey} and {@link SelfKey}.
  * <ul>
  * <li>
- * For {@link ContactKey} this API allows the insert/update, removal, changing of the
- * verification state, retrieving the keys (either created by or visible to the caller app)
- * operations.
+ * A {@link ContactKey} is a public key associated with a contact. It's used to end-to-end
+ * encrypt the communications between a user and the contact. This API allows operations on
+ * {@link ContactKey}s to insert/update, remove, change the verification state, and retrieving
+ * keys (either created by or visible to the caller app).
  * </li>
  * <li>
- * For {@link SelfKey} this API allows the insert/update, removal, retrieving the self keys
- * (either created by or visible to the caller app) operations.
+ * A {@link SelfKey} is a key for this device, so the key represents the owner of the device.
+ * This API allows operations on {@link SelfKey}s to insert/update, remove, and retrieving
+ * self keys (either created by or visible to the caller app).
  * </li>
  * </ul>
  * Keys are uniquely identified by:
@@ -71,7 +72,7 @@
  * ContactsProvider.
  */
 @FlaggedApi(Flags.FLAG_USER_KEYS)
-public class ContactKeysManager {
+public final class ContactKeysManager {
     /**
      * The authority for the contact keys provider.
      * @hide
@@ -354,9 +355,9 @@
 
 
     private static void validateVerificationState(int verificationState) {
-        if (verificationState != UNVERIFIED
-                && verificationState != VERIFICATION_FAILED
-                && verificationState != VERIFIED) {
+        if (verificationState != VERIFICATION_STATE_UNVERIFIED
+                && verificationState != VERIFICATION_STATE_VERIFICATION_FAILED
+                && verificationState != VERIFICATION_STATE_VERIFIED) {
             throw new IllegalArgumentException("Verification state value "
                     + verificationState + " is not supported");
         }
@@ -600,25 +601,25 @@
      * @hide
      */
     @IntDef(prefix = {"VERIFICATION_STATE_"}, value = {
-            UNVERIFIED,
-            VERIFICATION_FAILED,
-            VERIFIED
+            VERIFICATION_STATE_UNVERIFIED,
+            VERIFICATION_STATE_VERIFICATION_FAILED,
+            VERIFICATION_STATE_VERIFIED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface VerificationState {}
 
     /**
-     * Unverified state of a contact E2EE key.
+     * Unverified state of a contact end to end encrypted key.
      */
-    public static final int UNVERIFIED = 0;
+    public static final int VERIFICATION_STATE_UNVERIFIED = 0;
     /**
-     * Failed verification state of a contact E2EE key.
+     * Failed verification state of a contact end to end encrypted key.
      */
-    public static final int VERIFICATION_FAILED = 1;
+    public static final int VERIFICATION_STATE_VERIFICATION_FAILED = 1;
     /**
-     * Verified state of a contact E2EE key.
+     * Verified state of a contact end to end encrypted key.
      */
-    public static final int VERIFIED = 2;
+    public static final int VERIFICATION_STATE_VERIFIED = 2;
 
     /** @hide */
     public static final class ContactKeys {
@@ -791,7 +792,7 @@
     }
 
     /**
-     * A parcelable class encapsulating other users' E2EE contact key.
+     * A parcelable class encapsulating other users' end to end encrypted contact key.
      */
     public static final class ContactKey implements Parcelable {
         /**
@@ -1056,7 +1057,7 @@
     }
 
     /**
-     * A parcelable class encapsulating self E2EE contact key.
+     * A parcelable class encapsulating self end to end encrypted contact key.
      */
     public static final class SelfKey implements Parcelable {
         /**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 26f46cf..b026ce9 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -668,6 +668,23 @@
             "android.settings.MANAGE_APP_LONG_RUNNING_JOBS";
 
     /**
+     * Activity Action: Show settings to allow configuration of
+     * {@link Manifest.permission#RUN_BACKUP_JOBS} permission.
+     *
+     * Input: Optionally, the Intent's data URI can specify the application package name to
+     * directly invoke the management GUI specific to the package name. For example
+     * "package:com.my.app".
+     * <p>
+     * Output: When a package data uri is passed as input, the activity result is set to
+     * {@link android.app.Activity#RESULT_OK} if the permission was granted to the app. Otherwise,
+     * the result is set to {@link android.app.Activity#RESULT_CANCELED}.
+     */
+    @FlaggedApi(Flags.FLAG_BACKUP_TASKS_SETTINGS_SCREEN)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_REQUEST_RUN_BACKUP_JOBS =
+            "android.settings.REQUEST_RUN_BACKUP_JOBS";
+
+    /**
      * Activity Action: Show settings to allow configuration of cross-profile access for apps
      *
      * Input: Optionally, the Intent's data URI can specify the application package name to
@@ -11833,6 +11850,7 @@
          * Whether to enable camera extensions software fallback.
          * @hide
          */
+        @Readable
         public static final String CAMERA_EXTENSIONS_FALLBACK = "camera_extensions_fallback";
 
         /**
diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig
index 0f12b13..ea1ac27 100644
--- a/core/java/android/provider/flags.aconfig
+++ b/core/java/android/provider/flags.aconfig
@@ -13,3 +13,10 @@
     description: "This flag controls new E2EE contact keys API"
     bug: "290696572"
 }
+
+flag {
+    name: "backup_tasks_settings_screen"
+    namespace: "backstage_power"
+    description: "Add a new settings page for the RUN_BACKUP_JOBS permission."
+    bug: "320563660"
+}
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index 3cc7f5a..add575b 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -8,6 +8,13 @@
 }
 
 flag {
+    name: "enable_sharesheet_metadata_extra"
+    namespace: "intentresolver"
+    description: "This flag enables sharesheet metadata to be displayed to users."
+    bug: "318942069"
+}
+
+flag {
   name: "support_nfc_resolver"
   namespace: "systemui"
   description: "This flag controls the new NFC 'resolver' activity"
@@ -20,3 +27,10 @@
   description: "This flag controls content toggling in Chooser"
   bug: "302691505"
 }
+
+flag {
+  name: "enable_chooser_result"
+  namespace: "intentresolver"
+  description: "Provides additional callbacks with information about user actions in ChooserResult"
+  bug: "263474465"
+}
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index a6d3bb4..6410609 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -585,9 +585,7 @@
         }
 
         if (ClientFlags.fixLineHeightForLocale()) {
-            if (minimumFontMetrics == null) {
-                paint.getFontMetricsIntForLocale(fm);
-            } else {
+            if (minimumFontMetrics != null) {
                 fm.set(minimumFontMetrics);
                 // Because the font metrics is provided by public APIs, adjust the top/bottom with
                 // ascent/descent: top must be smaller than ascent, bottom must be larger than
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 99bd2ff..5986238 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -767,22 +767,14 @@
         }
 
         int defaultTop;
-        int defaultAscent;
-        int defaultDescent;
+        final int defaultAscent;
+        final int defaultDescent;
         int defaultBottom;
-        if (ClientFlags.fixLineHeightForLocale()) {
-            if (b.mMinimumFontMetrics != null) {
-                defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
-                defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
-                defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
-                defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
-            } else {
-                paint.getFontMetricsIntForLocale(fm);
-                defaultTop = fm.top;
-                defaultAscent = fm.ascent;
-                defaultDescent = fm.descent;
-                defaultBottom = fm.bottom;
-            }
+        if (ClientFlags.fixLineHeightForLocale() && b.mMinimumFontMetrics != null) {
+            defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
+            defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
+            defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
+            defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
 
             // Because the font metrics is provided by public APIs, adjust the top/bottom with
             // ascent/descent: top must be smaller than ascent, bottom must be larger than descent.
@@ -1043,10 +1035,10 @@
 
                     if (endPos < spanEnd) {
                         // preserve metrics for current span
-                        fmTop = fm.top;
-                        fmBottom = fm.bottom;
-                        fmAscent = fm.ascent;
-                        fmDescent = fm.descent;
+                        fmTop = Math.min(defaultTop, fm.top);
+                        fmBottom = Math.max(defaultBottom, fm.bottom);
+                        fmAscent = Math.min(defaultAscent, fm.ascent);
+                        fmDescent = Math.max(defaultDescent, fm.descent);
                     } else {
                         fmTop = fmBottom = fmAscent = fmDescent = 0;
                     }
@@ -1069,7 +1061,7 @@
                 && mLineCount < mMaximumVisibleLineCount) {
             final MeasuredParagraph measuredPara =
                     MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
-            if (ClientFlags.fixLineHeightForLocale()) {
+            if (defaultAscent != 0 && defaultDescent != 0) {
                 fm.top = defaultTop;
                 fm.ascent = defaultAscent;
                 fm.descent = defaultDescent;
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index fbadef3..0006139 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -86,6 +86,7 @@
  * that are currently attached and whether mirroring has been enabled.
  * </p>
  */
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
 public final class Display {
     private static final String TAG = "Display";
     private static final boolean DEBUG = false;
@@ -1998,6 +1999,7 @@
      * display power state. In SUSPEND states, updates are absolutely forbidden.
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static boolean isSuspendedState(int state) {
         return state == STATE_OFF || state == STATE_DOZE_SUSPEND || state == STATE_ON_SUSPEND;
     }
@@ -2007,6 +2009,7 @@
      * specified display power state.
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static boolean isDozeState(int state) {
         return state == STATE_DOZE || state == STATE_DOZE_SUSPEND;
     }
@@ -2016,6 +2019,7 @@
      * or {@link #STATE_VR}.
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static boolean isActiveState(int state) {
         return state == STATE_ON || state == STATE_VR;
     }
@@ -2024,6 +2028,7 @@
      * Returns true if the display is in an off state such as {@link #STATE_OFF}.
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static boolean isOffState(int state) {
         return state == STATE_OFF;
     }
@@ -2033,6 +2038,7 @@
      * or {@link #STATE_VR} or {@link #STATE_ON_SUSPEND}.
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static boolean isOnState(int state) {
         return state == STATE_ON || state == STATE_VR || state == STATE_ON_SUSPEND;
     }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index a9f1897..c22986b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30,6 +30,7 @@
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_UNKNOWN;
 import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH;
 import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE;
+import static android.view.flags.Flags.FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API;
 import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
 import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
 import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout;
@@ -1946,6 +1947,41 @@
     static final int TOOLTIP = 0x40000000;
 
     /** @hide */
+    @IntDef(prefix = { "CONTENT_SENSITIVITY_" }, value = {
+            CONTENT_SENSITIVITY_AUTO,
+            CONTENT_SENSITIVITY_SENSITIVE,
+            CONTENT_SENSITIVITY_NOT_SENSITIVE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ContentSensitivity {}
+
+    /**
+     * Automatically determine whether a view displays sensitive content. For example, available
+     * autofill hints (or some other signal) can be used to determine if this view
+     * displays sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_AUTO = 0x0;
+
+    /**
+     * The view displays sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_SENSITIVE = 0x1;
+
+    /**
+     * The view doesn't display sensitive content.
+     *
+     * @see #getContentSensitivity()
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 0x2;
+
+    /** @hide */
     @IntDef(flag = true, prefix = { "FOCUSABLES_" }, value = {
             FOCUSABLES_ALL,
             FOCUSABLES_TOUCH_MODE
@@ -3646,6 +3682,7 @@
      *           1                      PFLAG4_ROTARY_HAPTICS_ENABLED
      *          1                       PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT
      *         1                        PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT
+     *       11                         PFLAG4_CONTENT_SENSITIVITY_MASK
      * |-------|-------|-------|-------|
      */
 
@@ -3762,6 +3799,15 @@
      */
     private static final int PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT = 0x800000;
 
+    private static final int PFLAG4_CONTENT_SENSITIVITY_SHIFT = 24;
+
+    /**
+     * Mask for obtaining the bits which specify how to determine whether a view
+     * displays sensitive content or not.
+     */
+    private static final int PFLAG4_CONTENT_SENSITIVITY_MASK =
+            (CONTENT_SENSITIVITY_AUTO | CONTENT_SENSITIVITY_SENSITIVE
+                    | CONTENT_SENSITIVITY_NOT_SENSITIVE) << PFLAG4_CONTENT_SENSITIVITY_SHIFT;
     /* End of masks for mPrivateFlags4 */
 
     /** @hide */
@@ -10150,6 +10196,54 @@
     }
 
     /**
+     * Sets content sensitivity mode to determine whether this view displays sensitive content.
+     *
+     * @param mode {@link #CONTENT_SENSITIVITY_AUTO}, {@link #CONTENT_SENSITIVITY_NOT_SENSITIVE}
+     *                                            or {@link #CONTENT_SENSITIVITY_SENSITIVE}
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public final void setContentSensitivity(@ContentSensitivity int mode)  {
+        mPrivateFlags4 &= ~PFLAG4_CONTENT_SENSITIVITY_MASK;
+        mPrivateFlags4 |= ((mode << PFLAG4_CONTENT_SENSITIVITY_SHIFT)
+                & PFLAG4_CONTENT_SENSITIVITY_MASK);
+    }
+
+    /**
+     * Gets content sensitivity mode to determine whether this view displays sensitive content.
+     *
+     * <p>See {@link #setContentSensitivity(int)} and
+     * {@link #isContentSensitive()} for more info about this mode.
+     *
+     * @return {@link #CONTENT_SENSITIVITY_AUTO} by default, or value passed to
+     * {@link #setContentSensitivity(int)}.
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public @ContentSensitivity
+    final int getContentSensitivity() {
+        return (mPrivateFlags4 & PFLAG4_CONTENT_SENSITIVITY_MASK)
+                >> PFLAG4_CONTENT_SENSITIVITY_SHIFT;
+    }
+
+    /**
+     * Returns whether this view displays sensitive content, based
+     * on the value explicitly set by {@link #setContentSensitivity(int)}.
+     *
+     * @return whether the view displays sensitive content.
+     *
+     * @see #setContentSensitivity(int)
+     * @see #CONTENT_SENSITIVITY_AUTO
+     * @see #CONTENT_SENSITIVITY_SENSITIVE
+     * @see #CONTENT_SENSITIVITY_NOT_SENSITIVE
+     */
+    @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+    public final boolean isContentSensitive() {
+        if (getContentSensitivity() == CONTENT_SENSITIVITY_SENSITIVE) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Gets the mode for determining whether this view is important for content capture.
      *
      * <p>See {@link #setImportantForContentCapture(int)} and
diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java
index aa2474d7..3e0161a 100644
--- a/core/java/android/widget/EditText.java
+++ b/core/java/android/widget/EditText.java
@@ -16,9 +16,13 @@
 
 package android.widget;
 
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.os.Build;
 import android.text.Editable;
 import android.text.Selection;
 import android.text.Spannable;
@@ -29,6 +33,8 @@
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 
+import com.android.internal.R;
+
 /*
  * This is supposed to be a *very* thin veneer over TextView.
  * Do not make any changes here that do anything that a TextView
@@ -85,6 +91,11 @@
     private static final int ID_ITALIC = android.R.id.italic;
     private static final int ID_UNDERLINE = android.R.id.underline;
 
+    /** @hide */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public static final long LINE_HEIGHT_FOR_LOCALE = 303326708L;
+
     public EditText(Context context) {
         this(context, null);
     }
@@ -104,15 +115,39 @@
         final TypedArray a = theme.obtainStyledAttributes(attrs,
                 com.android.internal.R.styleable.EditText, defStyleAttr, defStyleRes);
 
-        final int n = a.getIndexCount();
-        for (int i = 0; i < n; ++i) {
-            int attr = a.getIndex(i);
-            switch (attr) {
-                case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
-                    mStyleShortcutsEnabled = a.getBoolean(attr, false);
-                    break;
+        try {
+            final int n = a.getIndexCount();
+            for (int i = 0; i < n; ++i) {
+                int attr = a.getIndex(i);
+                switch (attr) {
+                    case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
+                        mStyleShortcutsEnabled = a.getBoolean(attr, false);
+                        break;
+                }
             }
+        } finally {
+            a.recycle();
         }
+
+        boolean hasUseLocalePreferredLineHeightForMinimumInt = false;
+        boolean useLocalePreferredLineHeightForMinimumInt = false;
+        TypedArray tvArray = theme.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
+        try {
+            hasUseLocalePreferredLineHeightForMinimumInt =
+                    tvArray.hasValue(R.styleable.TextView_useLocalePreferredLineHeightForMinimum);
+            if (hasUseLocalePreferredLineHeightForMinimumInt) {
+                useLocalePreferredLineHeightForMinimumInt = tvArray.getBoolean(
+                        R.styleable.TextView_useLocalePreferredLineHeightForMinimum, false);
+            }
+        } finally {
+            tvArray.recycle();
+        }
+        if (!hasUseLocalePreferredLineHeightForMinimumInt) {
+            useLocalePreferredLineHeightForMinimumInt =
+                    CompatChanges.isChangeEnabled(LINE_HEIGHT_FOR_LOCALE);
+        }
+        setLocalePreferredLineHeightForMinimumUsed(useLocalePreferredLineHeightForMinimumInt);
     }
 
     @Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 9a4106d9..57e4e6a 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -867,6 +867,8 @@
 
     private boolean mUseBoundsForWidth;
     @Nullable private Paint.FontMetrics mMinimumFontMetrics;
+    @Nullable private Paint.FontMetrics mLocalePreferredFontMetrics;
+    private boolean mUseLocalePreferredLineHeightForMinimum;
 
     @ViewDebug.ExportedProperty(category = "text")
     @UnsupportedAppUsage
@@ -1617,6 +1619,11 @@
                 case com.android.internal.R.styleable.TextView_useBoundsForWidth:
                     mUseBoundsForWidth = a.getBoolean(attr, false);
                     hasUseBoundForWidthValue = true;
+                    break;
+                case com.android.internal.R.styleable
+                        .TextView_useLocalePreferredLineHeightForMinimum:
+                    mUseLocalePreferredLineHeightForMinimum = a.getBoolean(attr, false);
+                    break;
             }
         }
 
@@ -4992,6 +4999,41 @@
     }
 
     /**
+     * Returns true if the locale preferred line height is used for the minimum line height.
+     *
+     * @return true if using locale preferred line height for the minimum line height. Otherwise
+     *         false.
+     *
+     * @see #setLocalePreferredLineHeightForMinimumUsed(boolean)
+     * @see #setMinimumFontMetrics(Paint.FontMetrics)
+     * @see #getMinimumFontMetrics()
+     */
+    @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
+    public boolean isLocalePreferredLineHeightForMinimumUsed() {
+        return mUseLocalePreferredLineHeightForMinimum;
+    }
+
+    /**
+     * Set true if the locale preferred line height is used for the minimum line height.
+     *
+     * By setting this flag to true is equivalenet to call
+     * {@link #setMinimumFontMetrics(Paint.FontMetrics)} with the one obtained by
+     * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}.
+     *
+     * If custom minimum line height was specified by
+     * {@link #setMinimumFontMetrics(Paint.FontMetrics)}, this flag will be ignored.
+     *
+     * @param flag true for using locale preferred line height for the minimum line height.
+     * @see #isLocalePreferredLineHeightForMinimumUsed()
+     * @see #setMinimumFontMetrics(Paint.FontMetrics)
+     * @see #getMinimumFontMetrics()
+     */
+    @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
+    public void setLocalePreferredLineHeightForMinimumUsed(boolean flag) {
+        mUseLocalePreferredLineHeightForMinimum = flag;
+    }
+
+    /**
      * @return whether fallback line spacing is enabled, {@code true} by default
      *
      * @see #setFallbackLineSpacing(boolean)
@@ -10728,6 +10770,21 @@
         return alignment;
     }
 
+    private Paint.FontMetrics getResolvedMinimumFontMetrics() {
+        if (mMinimumFontMetrics != null) {
+            return mMinimumFontMetrics;
+        }
+        if (!mUseLocalePreferredLineHeightForMinimum) {
+            return null;
+        }
+
+        if (mLocalePreferredFontMetrics == null) {
+            mLocalePreferredFontMetrics = new Paint.FontMetrics();
+        }
+        mTextPaint.getFontMetricsForLocale(mLocalePreferredFontMetrics);
+        return mLocalePreferredFontMetrics;
+    }
+
     /**
      * The width passed in is now the desired layout width,
      * not the full view width with padding.
@@ -10792,7 +10849,8 @@
             if (hintBoring == UNKNOWN_BORING) {
                 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
                         isFallbackLineSpacingForBoringLayout(),
-                        mMinimumFontMetrics, mHintBoring);
+                        getResolvedMinimumFontMetrics(), mHintBoring);
+
                 if (hintBoring != null) {
                     mHintBoring = hintBoring;
                 }
@@ -10842,7 +10900,8 @@
                         .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                                 mLineBreakStyle, mLineBreakWordStyle))
                         .setUseBoundsForWidth(mUseBoundsForWidth)
-                        .setMinimumFontMetrics(mMinimumFontMetrics);
+                        .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
+
                 if (shouldEllipsize) {
                     builder.setEllipsize(mEllipsize)
                             .setEllipsizedWidth(ellipsisWidth);
@@ -10907,12 +10966,13 @@
                     .setUseBoundsForWidth(mUseBoundsForWidth)
                     .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
                     .setEllipsizedWidth(ellipsisWidth)
-                    .setMinimumFontMetrics(mMinimumFontMetrics);
+                    .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
             result = builder.build();
         } else {
             if (boring == UNKNOWN_BORING) {
                 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
-                        isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring);
+                        isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
+                        mBoring);
                 if (boring != null) {
                     mBoring = boring;
                 }
@@ -10926,7 +10986,7 @@
                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad, null, wantWidth,
                                 isFallbackLineSpacingForBoringLayout(),
-                                mUseBoundsForWidth, mMinimumFontMetrics);
+                                mUseBoundsForWidth, getResolvedMinimumFontMetrics());
                     } else {
                         result = new BoringLayout(
                                 mTransformed,
@@ -10941,7 +11001,7 @@
                                 null,
                                 boring,
                                 mUseBoundsForWidth,
-                                mMinimumFontMetrics);
+                                getResolvedMinimumFontMetrics());
                     }
 
                     if (useSaved) {
@@ -10953,7 +11013,7 @@
                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad, effectiveEllipsize,
                                 ellipsisWidth, isFallbackLineSpacingForBoringLayout(),
-                                mUseBoundsForWidth, mMinimumFontMetrics);
+                                mUseBoundsForWidth, getResolvedMinimumFontMetrics());
                     } else {
                         result = new BoringLayout(
                                 mTransformed,
@@ -10968,7 +11028,7 @@
                                 effectiveEllipsize,
                                 boring,
                                 mUseBoundsForWidth,
-                                mMinimumFontMetrics);
+                                getResolvedMinimumFontMetrics());
                     }
                 }
             }
@@ -10988,7 +11048,7 @@
                     .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                             mLineBreakStyle, mLineBreakWordStyle))
                     .setUseBoundsForWidth(mUseBoundsForWidth)
-                    .setMinimumFontMetrics(mMinimumFontMetrics);
+                    .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
             if (shouldEllipsize) {
                 builder.setEllipsize(effectiveEllipsize)
                         .setEllipsizedWidth(ellipsisWidth);
@@ -11116,7 +11176,8 @@
 
             if (des < 0) {
                 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
-                        isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring);
+                        isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
+                        mBoring);
                 if (boring != null) {
                     mBoring = boring;
                 }
@@ -11156,7 +11217,7 @@
 
                 if (hintDes < 0) {
                     hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
-                            isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics,
+                            isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
                             mHintBoring);
                     if (hintBoring != null) {
                         mHintBoring = hintBoring;
@@ -11370,7 +11431,7 @@
                 .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                         mLineBreakStyle, mLineBreakWordStyle))
                 .setUseBoundsForWidth(mUseBoundsForWidth)
-                .setMinimumFontMetrics(mMinimumFontMetrics);
+                .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
 
         final StaticLayout layout = layoutBuilder.build();
 
diff --git a/core/java/android/widget/flags/notification_widget_flags.aconfig b/core/java/android/widget/flags/notification_widget_flags.aconfig
index 9f0b7c3..bfe3d05 100644
--- a/core/java/android/widget/flags/notification_widget_flags.aconfig
+++ b/core/java/android/widget/flags/notification_widget_flags.aconfig
@@ -5,4 +5,14 @@
    namespace: "systemui"
    description: "Enables notification specific LinearLayout optimization"
    bug: "316110233"
+}
+
+flag {
+  name: "call_style_set_data_async"
+  namespace: "systemui"
+  description: "Offloads caller icon drawable loading to the background thread"
+  bug: "293961072"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/os/BackgroundThread.java b/core/java/com/android/internal/os/BackgroundThread.java
index 72da819..b75daed 100644
--- a/core/java/com/android/internal/os/BackgroundThread.java
+++ b/core/java/com/android/internal/os/BackgroundThread.java
@@ -27,6 +27,7 @@
 /**
  * Shared singleton background thread for each process.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class BackgroundThread extends HandlerThread {
     private static final long SLOW_DISPATCH_THRESHOLD_MS = 10_000;
     private static final long SLOW_DELIVERY_THRESHOLD_MS = 30_000;
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index aa60cc9..0b7593a 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -297,9 +297,20 @@
         }
     }
 
+    public static class EventLogger {
+        /**
+         * Records a statsd event when the batterystats config file is written to disk.
+         */
+        public void writeCommitSysConfigFile(long startTimeMs) {
+            com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
+                    "batterystats", SystemClock.uptimeMillis() - startTimeMs);
+        }
+    }
+
     private TraceDelegate mTracer;
     private int mTraceLastState = 0;
     private int mTraceLastState2 = 0;
+    private final EventLogger mEventLogger;
 
     /**
      * Constructor
@@ -311,8 +322,16 @@
     public BatteryStatsHistory(File systemDir, int maxHistoryFiles, int maxHistoryBufferSize,
             HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
             MonotonicClock monotonicClock) {
+        this(systemDir, maxHistoryFiles, maxHistoryBufferSize,
+                stepDetailsCalculator, clock, monotonicClock, new TraceDelegate(),
+                new EventLogger());
+    }
+
+    public BatteryStatsHistory(File systemDir, int maxHistoryFiles, int maxHistoryBufferSize,
+            HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
+            MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) {
         this(Parcel.obtain(), systemDir, maxHistoryFiles, maxHistoryBufferSize,
-                stepDetailsCalculator, clock, monotonicClock, new TraceDelegate());
+                stepDetailsCalculator, clock, monotonicClock, tracer, eventLogger);
         initHistoryBuffer();
     }
 
@@ -320,15 +339,15 @@
     public BatteryStatsHistory(Parcel historyBuffer, File systemDir,
             int maxHistoryFiles, int maxHistoryBufferSize,
             HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
-            MonotonicClock monotonicClock, TraceDelegate tracer) {
+            MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) {
         this(historyBuffer, systemDir, maxHistoryFiles, maxHistoryBufferSize, stepDetailsCalculator,
-                clock, monotonicClock, tracer, null);
+                clock, monotonicClock, tracer, eventLogger, null);
     }
 
     private BatteryStatsHistory(Parcel historyBuffer, File systemDir,
             int maxHistoryFiles, int maxHistoryBufferSize,
             HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
-            MonotonicClock monotonicClock, TraceDelegate tracer,
+            MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger,
             BatteryStatsHistory writableHistory) {
         mHistoryBuffer = historyBuffer;
         mSystemDir = systemDir;
@@ -338,6 +357,7 @@
         mTracer = tracer;
         mClock = clock;
         mMonotonicClock = monotonicClock;
+        mEventLogger = eventLogger;
         mWritableHistory = writableHistory;
         if (mWritableHistory != null) {
             mMutable = false;
@@ -394,19 +414,21 @@
             HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
             MonotonicClock monotonicClock) {
         this(maxHistoryFiles, maxHistoryBufferSize, stepDetailsCalculator, clock, monotonicClock,
-                new TraceDelegate());
+                new TraceDelegate(), new EventLogger());
     }
 
     @VisibleForTesting
     public BatteryStatsHistory(int maxHistoryFiles, int maxHistoryBufferSize,
             HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
-            MonotonicClock monotonicClock, TraceDelegate traceDelegate) {
+            MonotonicClock monotonicClock, TraceDelegate traceDelegate,
+            EventLogger eventLogger) {
         mMaxHistoryFiles = maxHistoryFiles;
         mMaxHistoryBufferSize = maxHistoryBufferSize;
         mStepDetailsCalculator = stepDetailsCalculator;
         mTracer = traceDelegate;
         mClock = clock;
         mMonotonicClock = monotonicClock;
+        mEventLogger = eventLogger;
 
         mHistoryBuffer = Parcel.obtain();
         mSystemDir = null;
@@ -425,6 +447,7 @@
         mSystemDir = null;
         mHistoryDir = null;
         mStepDetailsCalculator = null;
+        mEventLogger = new EventLogger();
         mWritableHistory = null;
         mMutable = false;
 
@@ -482,7 +505,7 @@
             historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
 
             return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null,
-                    null, this);
+                    null, mEventLogger, this);
         }
     }
 
@@ -2154,8 +2177,7 @@
                         + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs)
                         + " bytes:" + p.dataSize());
             }
-            com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
-                    "batterystats", SystemClock.uptimeMillis() - startTimeMs);
+            mEventLogger.writeCommitSysConfigFile(startTimeMs);
         } catch (IOException e) {
             Slog.w(TAG, "Error writing battery statistics", e);
             file.failWrite(fos);
@@ -2164,6 +2186,7 @@
         }
     }
 
+
     /**
      * Returns the total number of history tags in the tag pool.
      */
diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java
index 33a9d54..064609f 100644
--- a/core/java/com/android/internal/os/LongMultiStateCounter.java
+++ b/core/java/com/android/internal/os/LongMultiStateCounter.java
@@ -55,11 +55,12 @@
  *
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
+        "com.android.hoststubgen.nativesubstitution.LongMultiStateCounter_host")
 public final class LongMultiStateCounter implements Parcelable {
 
-    private static final NativeAllocationRegistry sRegistry =
-            NativeAllocationRegistry.createMalloced(
-                    LongMultiStateCounter.class.getClassLoader(), native_getReleaseFunc());
+    private static NativeAllocationRegistry sRegistry;
 
     private final int mStateCount;
 
@@ -71,16 +72,33 @@
         Preconditions.checkArgumentPositive(stateCount, "stateCount must be greater than 0");
         mStateCount = stateCount;
         mNativeObject = native_init(stateCount);
-        sRegistry.registerNativeAllocation(this, mNativeObject);
+        registerNativeAllocation();
     }
 
     private LongMultiStateCounter(Parcel in) {
         mNativeObject = native_initFromParcel(in);
-        sRegistry.registerNativeAllocation(this, mNativeObject);
+        registerNativeAllocation();
 
         mStateCount = native_getStateCount(mNativeObject);
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
+    private void registerNativeAllocation() {
+        if (sRegistry == null) {
+            synchronized (LongMultiStateCounter.class) {
+                if (sRegistry == null) {
+                    sRegistry = NativeAllocationRegistry.createMalloced(
+                            LongMultiStateCounter.class.getClassLoader(), native_getReleaseFunc());
+                }
+            }
+        }
+        sRegistry.registerNativeAllocation(this, mNativeObject);
+    }
+
+    private void registerNativeAllocation$ravenwood() {
+        // No-op under ravenwood
+    }
+
     public int getStateCount() {
         return mStateCount;
     }
@@ -221,10 +239,10 @@
     private static native long native_getCount(long nativeObject, int state);
 
     @FastNative
-    private native String native_toString(long nativeObject);
+    private static native String native_toString(long nativeObject);
 
     @FastNative
-    private native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
+    private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
 
     @FastNative
     private static native long native_initFromParcel(Parcel parcel);
diff --git a/core/java/com/android/internal/power/EnergyConsumerStats.java b/core/java/com/android/internal/power/EnergyConsumerStats.java
index e2098dd..764908d 100644
--- a/core/java/com/android/internal/power/EnergyConsumerStats.java
+++ b/core/java/com/android/internal/power/EnergyConsumerStats.java
@@ -44,6 +44,7 @@
  * This class doesn't use a TimeBase, and instead requires manual decisions about when to
  * accumulate since it is trivial. However, in the future, a TimeBase could be used instead.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class EnergyConsumerStats {
     private static final String TAG = "MeasuredEnergyStats";
 
diff --git a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
index ddf7a67..56d3fbb 100644
--- a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
+++ b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
@@ -100,7 +100,7 @@
     return asLongMultiStateCounter(nativePtr)->getCount(state);
 }
 
-static jobject native_toString(JNIEnv *env, jobject self, jlong nativePtr) {
+static jobject native_toString(JNIEnv *env, jclass, jlong nativePtr) {
     return env->NewStringUTF(asLongMultiStateCounter(nativePtr)->toString().c_str());
 }
 
@@ -118,7 +118,7 @@
         }                                     \
     }
 
-static void native_writeToParcel(JNIEnv *env, jobject self, jlong nativePtr, jobject jParcel,
+static void native_writeToParcel(JNIEnv *env, jclass, jlong nativePtr, jobject jParcel,
                                  jint flags) {
     battery::LongMultiStateCounter *counter = asLongMultiStateCounter(nativePtr);
     ndk::ScopedAParcel parcel(AParcel_fromJavaParcel(env, jParcel));
diff --git a/core/proto/android/hardware/sensorprivacy.proto b/core/proto/android/hardware/sensorprivacy.proto
index e368c6a..9359528 100644
--- a/core/proto/android/hardware/sensorprivacy.proto
+++ b/core/proto/android/hardware/sensorprivacy.proto
@@ -91,9 +91,6 @@
     enum StateType {
         ENABLED = 1;
         DISABLED = 2;
-        AUTO_DRIVER_ASSISTANCE_HELPFUL_APPS = 3;
-        AUTO_DRIVER_ASSISTANCE_REQUIRED_APPS = 4;
-        AUTO_DRIVER_ASSISTANCE_APPS = 5;
     }
 
     // DEPRECATED
@@ -137,4 +134,4 @@
     // Source for which sensor privacy was toggled.
     optional Source source = 1;
 
-}
+}
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 316001a..a425bb0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1730,16 +1730,6 @@
         android:description="@string/permdesc_cameraHeadlessSystemUser"
         android:protectionLevel="signature" />
 
-
-    <!-- @SystemApi Allows camera access of allowlisted driver assistance apps
-         to be controlled separately.
-         <p> Not for use by third-party applications.
-         @FlaggedApi("com.android.internal.camera.flags.privacy_allowlist")
-         @hide
-    -->
-    <permission android:name="android.permission.CAMERA_PRIVACY_ALLOWLIST"
-        android:protectionLevel="signature|privileged" />
-
     <!-- ====================================================================== -->
     <!-- Permissions for accessing the device sensors                           -->
     <!-- ====================================================================== -->
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 45861a3..41bc825 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5857,6 +5857,23 @@
           use glyph bound's as a source of text width.  -->
         <!-- @FlaggedApi("com.android.text.flags.use_bounds_for_width") -->
         <attr name="useBoundsForWidth" format="boolean" />
+        <!-- Whether to use the locale preferred line height for the minimum line height.
+
+          This flag is useful for preventing jitter of entering letters into empty EditText.
+          The line height of the text is determined by the font files used for drawing text in a
+          line. However, in case of the empty text case, the line height cannot be determined and
+          the default line height: usually it is came from a font of Latin script. By making this
+          attribute to true, the TextView/EditText uses a line height that is likely used for the
+          locale associated with the widget. For example, if the system locale is Japanese, the
+          height of the EditText will be adjusted to meet the height of the Japanese font even if
+          the text is empty.
+
+          The default value for EditText is true if targetSdkVersion is
+          {@link android.os.Build.VERSION_CODE#VANILLA_ICE_CREAM} or later, otherwise false.
+          For other TextViews, the default value is false.
+        -->
+        <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") -->
+        <attr name="useLocalePreferredLineHeightForMinimum" format="boolean" />
     </declare-styleable>
     <declare-styleable name="TextViewAppearance">
         <!-- Base text color, typeface, size, and style. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index d4e727e..c6bc589 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2141,6 +2141,12 @@
         <item>com.android.location.fused</item>
     </string-array>
 
+   <!-- Package name of the extension software fallback. -->
+    <string name="config_extensionFallbackPackageName" translatable="false"></string>
+
+   <!-- Service name of the extension software fallback. -->
+    <string name="config_extensionFallbackServiceName" translatable="false"></string>
+
     <!-- Package name(s) of Advanced Driver Assistance applications. These packages have additional
     management of access to location, specific to driving assistance use-cases. They must be system
     packages. This configuration is only applicable to devices that declare
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index d0216b30..dbe7196 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -172,6 +172,35 @@
     <integer name="config_satellite_nb_iot_inactivity_timeout_millis">180000</integer>
     <java-symbol type="integer" name="config_satellite_nb_iot_inactivity_timeout_millis" />
 
+    <!-- The time duration in millis needed to switch the modem image from TN to NTN. -->
+    <integer name="config_satellite_modem_image_switching_duration_millis">20000</integer>
+    <java-symbol type="integer" name="config_satellite_modem_image_switching_duration_millis" />
+
+    <!-- The time duration in millis after which Telephony will abort the datagram sending requests.
+         Telephony starts a timer when receiving a datagram sending request in either OFF, IDLE, or
+         NOT_CONNECTED state. In NOT_CONNECTED, the duration of the timer is given by this config.
+         In OFF or IDLE state, the duration of the timer is the sum of this config and the
+         config_satellite_modem_image_switching_duration_millis.
+         -->
+    <integer name="config_datagram_wait_for_connected_state_timeout_millis">60000</integer>
+    <java-symbol type="integer" name="config_datagram_wait_for_connected_state_timeout_millis" />
+
+    <!-- The time duration in millis after which Telephony will stop waiting for the response of the
+         satellite enable request from modem, and send failure response to the client that has
+         requested Telephony to enable satellite.
+         -->
+    <integer name="config_wait_for_satellite_enabling_response_timeout_millis">180000</integer>
+    <java-symbol type="integer" name="config_wait_for_satellite_enabling_response_timeout_millis" />
+
+    <!-- The time duration in millis after which Telephony will abort the datagram sending requests
+         and send failure response to the client that has requested sending the datagrams. Telephony
+         starts a timer after pushing down the datagram sending request to modem. Before expiry, the
+         timer will be stopped when Telephony receives the response for the sending request from
+         modem.
+         -->
+    <integer name="config_wait_for_datagram_sending_response_timeout_millis">180000</integer>
+    <java-symbol type="integer" name="config_wait_for_datagram_sending_response_timeout_millis" />
+
     <!-- The timeout duration in milliseconds to determine whether to recommend Dialer to show the
          emergency messaging option to users.
 
@@ -183,6 +212,11 @@
     <integer name="config_emergency_call_wait_for_connection_timeout_millis">20000</integer>
     <java-symbol type="integer" name="config_emergency_call_wait_for_connection_timeout_millis" />
 
+    <!-- Indicates the data limit in bytes that can be used for bootstrap sim until factory reset.
+         -1 means unlimited. -->
+    <integer name="config_esim_bootstrap_data_limit_bytes">-1</integer>
+    <java-symbol type="integer" name="config_esim_bootstrap_data_limit_bytes" />
+
     <!-- Telephony config for the PLMNs of all satellite providers. This is used by satellite modem
          to identify providers that should be ignored if the carrier config
          carrier_supported_satellite_services_per_provider_bundle does not support them.
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 0a6779a9..5ee5555 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -153,6 +153,8 @@
     <public name="requireContentUriPermissionFromCaller" />
     <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") -->
     <public name="languageSettingsActivity"/>
+    <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") -->
+    <public name="useLocalePreferredLineHeightForMinimum"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3df7570..3d19c85 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2154,6 +2154,8 @@
   <java-symbol type="string" name="config_systemImageEditor" />
   <java-symbol type="string" name="config_datause_iface" />
   <java-symbol type="string" name="config_activityRecognitionHardwarePackageName" />
+  <java-symbol type="string" name="config_extensionFallbackPackageName" />
+  <java-symbol type="string" name="config_extensionFallbackServiceName" />
   <java-symbol type="string" name="config_fusedLocationProviderPackageName" />
   <java-symbol type="string" name="config_gnssLocationProviderPackageName" />
   <java-symbol type="string" name="config_geocoderProviderPackageName" />
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index ee1a4ac..861f719 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -217,6 +217,7 @@
         "src/android/os/**/*.java",
         "src/android/telephony/PinResultTest.java",
         "src/android/util/**/*.java",
+        "src/android/view/DisplayTest.java",
         "src/android/view/DisplayInfoTest.java",
         "src/com/android/internal/logging/**/*.java",
         "src/com/android/internal/os/**/*.java",
diff --git a/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.java b/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.java
new file mode 100644
index 0000000..e12ca24a
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.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 android.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class BluetoothBatteryStatsTest {
+
+    @Test
+    public void parcelability() {
+        BluetoothBatteryStats stats = new BluetoothBatteryStats(List.of(
+                new BluetoothBatteryStats.UidStats(42, 100, 200, 300, 400, 500),
+                new BluetoothBatteryStats.UidStats(99, 600, 700, 800, 900, 999)
+        ));
+
+        Parcel parcel = Parcel.obtain();
+        stats.writeToParcel(parcel, 0);
+        byte[] bytes = parcel.marshall();
+        parcel.recycle();
+
+        parcel = Parcel.obtain();
+        parcel.unmarshall(bytes, 0, bytes.length);
+        parcel.setDataPosition(0);
+
+        BluetoothBatteryStats actual = new BluetoothBatteryStats(parcel);
+
+        assertThat(actual.getUidStats()).hasSize(2);
+
+        BluetoothBatteryStats.UidStats uid1 = actual.getUidStats().stream()
+                .filter(s->s.uid == 42).findFirst().get();
+
+        assertThat(uid1.scanTimeMs).isEqualTo(100);
+        assertThat(uid1.unoptimizedScanTimeMs).isEqualTo(200);
+        assertThat(uid1.scanResultCount).isEqualTo(300);
+        assertThat(uid1.rxTimeMs).isEqualTo(400);
+        assertThat(uid1.txTimeMs).isEqualTo(500);
+
+        BluetoothBatteryStats.UidStats uid2 = actual.getUidStats().stream()
+                .filter(s->s.uid == 99).findFirst().get();
+        assertThat(uid2.scanTimeMs).isEqualTo(600);
+    }
+}
diff --git a/core/tests/coretests/src/android/os/WakeLockStatsTest.java b/core/tests/coretests/src/android/os/WakeLockStatsTest.java
new file mode 100644
index 0000000..2675ba0
--- /dev/null
+++ b/core/tests/coretests/src/android/os/WakeLockStatsTest.java
@@ -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.
+ */
+
+package android.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class WakeLockStatsTest {
+
+    @Test
+    public void parcelablity() {
+        WakeLockStats wakeLockStats = new WakeLockStats(
+                List.of(new WakeLockStats.WakeLock(1, "foo", 200, 3000, 40000),
+                        new WakeLockStats.WakeLock(2, "bar", 500, 6000, 70000)));
+
+        Parcel parcel = Parcel.obtain();
+        wakeLockStats.writeToParcel(parcel, 0);
+        byte[] bytes = parcel.marshall();
+        parcel.recycle();
+
+        parcel = Parcel.obtain();
+        parcel.unmarshall(bytes, 0, bytes.length);
+        parcel.setDataPosition(0);
+
+        WakeLockStats actual = WakeLockStats.CREATOR.createFromParcel(parcel);
+        assertThat(actual.getWakeLocks()).hasSize(2);
+        WakeLockStats.WakeLock wl1 = actual.getWakeLocks().get(0);
+        assertThat(wl1.uid).isEqualTo(1);
+        assertThat(wl1.name).isEqualTo("foo");
+        assertThat(wl1.timesAcquired).isEqualTo(200);
+        assertThat(wl1.totalTimeHeldMs).isEqualTo(3000);
+        assertThat(wl1.timeHeldMs).isEqualTo(40000);
+
+        WakeLockStats.WakeLock wl2 = actual.getWakeLocks().get(1);
+        assertThat(wl2.uid).isEqualTo(2);
+    }
+}
diff --git a/core/tests/coretests/src/android/view/DisplayTest.java b/core/tests/coretests/src/android/view/DisplayTest.java
new file mode 100644
index 0000000..4d2a1c4
--- /dev/null
+++ b/core/tests/coretests/src/android/view/DisplayTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.google.common.truth.Truth.assertWithMessage;
+
+import android.util.DebugUtils;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.ArrayUtils;
+
+import org.junit.Test;
+
+import java.util.function.IntFunction;
+
+@SmallTest
+public class DisplayTest {
+    private static final int[] DISPLAY_STATES = {
+            Display.STATE_UNKNOWN,
+            Display.STATE_OFF,
+            Display.STATE_ON,
+            Display.STATE_DOZE,
+            Display.STATE_DOZE_SUSPEND,
+            Display.STATE_VR,
+            Display.STATE_ON_SUSPEND
+    };
+
+    @Test
+    public void isSuspendedState() {
+        assertOnlyTrueForStates(
+                Display::isSuspendedState,
+                Display.STATE_OFF,
+                Display.STATE_DOZE_SUSPEND,
+                Display.STATE_ON_SUSPEND
+        );
+    }
+
+    @Test
+    public void isDozeState() {
+        assertOnlyTrueForStates(
+                Display::isDozeState,
+                Display.STATE_DOZE,
+                Display.STATE_DOZE_SUSPEND
+        );
+    }
+
+    @Test
+    public void isActiveState() {
+        assertOnlyTrueForStates(
+                Display::isActiveState,
+                Display.STATE_ON,
+                Display.STATE_VR
+        );
+    }
+
+    @Test
+    public void isOffState() {
+        assertOnlyTrueForStates(
+                Display::isOffState,
+                Display.STATE_OFF
+        );
+    }
+
+    @Test
+    public void isOnState() {
+        assertOnlyTrueForStates(
+                Display::isOnState,
+                Display.STATE_ON,
+                Display.STATE_VR,
+                Display.STATE_ON_SUSPEND
+        );
+    }
+
+    private void assertOnlyTrueForStates(IntFunction<Boolean> function, int... trueStates) {
+        for (int state : DISPLAY_STATES) {
+            boolean actual = function.apply(state);
+            boolean expected = ArrayUtils.contains(trueStates, state);
+            assertWithMessage("Unexpected return for Display.STATE_"
+                    + DebugUtils.constantToString(Display.class, "STATE_", state))
+                    .that(actual).isEqualTo(expected);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index 610b8ae..0b0fd66 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -19,10 +19,12 @@
 import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.annotation.NonNull;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Region;
+import android.hardware.usb.UsbDevice;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteCallback;
@@ -237,4 +239,15 @@
             int accessibilityWindowId,
             SurfaceControl sc,
             IAccessibilityInteractionConnectionCallback callback) {}
+
+    @Override
+    public void connectBluetoothBrailleDisplay(String bluetoothAddress,
+            IBrailleDisplayController controller) {}
+
+    @Override
+    public void connectUsbBrailleDisplay(UsbDevice usbDevice,
+            IBrailleDisplayController controller) {}
+
+    @Override
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {}
 }
diff --git a/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java b/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java
new file mode 100644
index 0000000..8bdf4c6
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java
@@ -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.internal.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.Executor;
+
+public class BackgroundThreadTest {
+
+    @Rule
+    public final RavenwoodRule mRavenwood =
+            new RavenwoodRule.Builder().setProvideMainThread(true).build();
+
+    @Test
+    public void test_get() {
+        BackgroundThread thread = BackgroundThread.get();
+        assertThat(thread.getLooper()).isNotEqualTo(Looper.getMainLooper());
+    }
+
+    @Test
+    public void test_getHandler() {
+        Handler handler = BackgroundThread.getHandler();
+        ConditionVariable done = new ConditionVariable();
+        handler.post(done::open);
+        boolean success = done.block(5000);
+        assertThat(success).isTrue();
+    }
+
+    @Test
+    public void test_getExecutor() {
+        Executor executor = BackgroundThread.getExecutor();
+        ConditionVariable done = new ConditionVariable();
+        executor.execute(done::open);
+        boolean success = done.block(5000);
+        assertThat(success).isTrue();
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
index e064e74..78ef92b 100644
--- a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
@@ -22,7 +22,6 @@
 
 import android.os.BadParcelableException;
 import android.os.Parcel;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
@@ -34,7 +33,6 @@
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
-@IgnoreUnderRavenwood(blockedBy = LongMultiStateCounterTest.class)
 public class LongMultiStateCounterTest {
     @Rule
     public final RavenwoodRule mRavenwood = new RavenwoodRule();
diff --git a/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java b/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java
index ae2ef0cb..9c337d7 100644
--- a/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java
@@ -34,7 +34,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.os.Parcel;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.view.Display;
 
@@ -49,7 +48,6 @@
  * Test class for {@link EnergyConsumerStats}.
  */
 @SmallTest
-@IgnoreUnderRavenwood(reason = "Needs kernel support")
 public class EnergyConsumerStatsTest {
     @Rule
     public final RavenwoodRule mRavenwood = new RavenwoodRule();
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 0baaff0..d8713f7a 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -575,6 +575,8 @@
         <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
         <!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp -->
         <permission name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/>
+        <!-- Permission required for CTS test - PackageManagerTest -->
+        <permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e5045ae..70b2f21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2959,13 +2959,17 @@
     }
 
     public void goToFullscreenFromSplit() {
-        boolean leftOrTop;
-        if (mSideStage.isFocused()) {
-            leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+        // If main stage is focused, toEnd = true if
+        // mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT. Otherwise toEnd = false
+        // If side stage is focused, toEnd = true if
+        // mSideStagePosition = SPLIT_POSITION_TOP_OR_LEFT. Otherwise toEnd = false
+        final boolean toEnd;
+        if (mMainStage.isFocused()) {
+            toEnd = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
         } else {
-            leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
+            toEnd = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
         }
-        mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT);
+        mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_FULLSCREEN_SHORTCUT);
     }
 
     /** Move the specified task to fullscreen, regardless of focus state. */
diff --git a/libs/input/SpriteIcon.h b/libs/input/SpriteIcon.h
index 9e6cc81..0939af4 100644
--- a/libs/input/SpriteIcon.h
+++ b/libs/input/SpriteIcon.h
@@ -27,33 +27,20 @@
  * Icon that a sprite displays, including its hotspot.
  */
 struct SpriteIcon {
-    inline SpriteIcon() : style(PointerIconStyle::TYPE_NULL), hotSpotX(0), hotSpotY(0) {}
-    inline SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX,
-                      float hotSpotY, bool drawNativeDropShadow)
+    explicit SpriteIcon() = default;
+    explicit SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX,
+                        float hotSpotY, bool drawNativeDropShadow)
           : bitmap(bitmap),
             style(style),
             hotSpotX(hotSpotX),
             hotSpotY(hotSpotY),
             drawNativeDropShadow(drawNativeDropShadow) {}
 
-    graphics::Bitmap bitmap;
-    PointerIconStyle style;
-    float hotSpotX;
-    float hotSpotY;
-    bool drawNativeDropShadow;
-
-    inline SpriteIcon copy() const {
-        return SpriteIcon(bitmap.copy(ANDROID_BITMAP_FORMAT_RGBA_8888), style, hotSpotX, hotSpotY,
-                          drawNativeDropShadow);
-    }
-
-    inline void reset() {
-        bitmap.reset();
-        style = PointerIconStyle::TYPE_NULL;
-        hotSpotX = 0;
-        hotSpotY = 0;
-        drawNativeDropShadow = false;
-    }
+    graphics::Bitmap bitmap{};
+    PointerIconStyle style{PointerIconStyle::TYPE_NULL};
+    float hotSpotX{};
+    float hotSpotY{};
+    bool drawNativeDropShadow{false};
 
     inline bool isValid() const { return bitmap.isValid() && !bitmap.isEmpty(); }
 
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
new file mode 100644
index 0000000..c4b38c7
--- /dev/null
+++ b/media/java/android/media/flags/projection.aconfig
@@ -0,0 +1,11 @@
+package: "com.android.media.flags"
+
+# Project link: https://gantry.corp.google.com/projects/android_platform_window_surfaces/changes
+
+flag {
+    name: "limit_manage_media_projection"
+    namespace: "lse_desktop_experience"
+    description: "Limit signature permission manage_media_projection to the SystemUI role"
+    bug: "323008518"
+    is_fixed_read_only: true
+}
diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java
index 5ed8d40..f1c5c9d 100644
--- a/media/java/android/media/metrics/EditingEndedEvent.java
+++ b/media/java/android/media/metrics/EditingEndedEvent.java
@@ -20,6 +20,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
+import android.annotation.LongDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Bundle;
@@ -27,6 +28,8 @@
 import android.os.Parcelable;
 
 import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
 /** Event for an editing operation having ended. */
@@ -156,14 +159,66 @@
     @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events.
     private final long mTimeSinceCreatedMillis;
 
+    private final ArrayList<MediaItemInfo> mInputMediaItemInfos;
+    @Nullable private final MediaItemInfo mOutputMediaItemInfo;
+
+    /** @hide */
+    @LongDef(
+            prefix = {"OPERATION_TYPE_"},
+            flag = true,
+            value = {
+                OPERATION_TYPE_VIDEO_TRANSCODE,
+                OPERATION_TYPE_AUDIO_TRANSCODE,
+                OPERATION_TYPE_VIDEO_EDIT,
+                OPERATION_TYPE_AUDIO_EDIT,
+                OPERATION_TYPE_VIDEO_TRANSMUX,
+                OPERATION_TYPE_AUDIO_TRANSMUX,
+                OPERATION_TYPE_PAUSED,
+                OPERATION_TYPE_RESUMED,
+            })
+    @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    public @interface OperationType {}
+
+    /** Input video was decoded and re-encoded. */
+    public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1;
+
+    /** Input audio was decoded and re-encoded. */
+    public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 1L << 1;
+
+    /** Input video was edited. */
+    public static final long OPERATION_TYPE_VIDEO_EDIT = 1L << 2;
+
+    /** Input audio was edited. */
+    public static final long OPERATION_TYPE_AUDIO_EDIT = 1L << 3;
+
+    /** Input video samples were writted (muxed) directly to the output file without transcoding. */
+    public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 1L << 4;
+
+    /** Input audio samples were written (muxed) directly to the output file without transcoding. */
+    public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 1L << 5;
+
+    /** The editing operation was paused before it completed. */
+    public static final long OPERATION_TYPE_PAUSED = 1L << 6;
+
+    /** The editing operation resumed a previous (paused) operation. */
+    public static final long OPERATION_TYPE_RESUMED = 1L << 7;
+
+    private final @OperationType long mOperationTypes;
+
     private EditingEndedEvent(
             @FinalState int finalState,
             @ErrorCode int errorCode,
             long timeSinceCreatedMillis,
+            ArrayList<MediaItemInfo> inputMediaItemInfos,
+            @Nullable MediaItemInfo outputMediaItemInfo,
+            @OperationType long operationTypes,
             @NonNull Bundle extras) {
         mFinalState = finalState;
         mErrorCode = errorCode;
         mTimeSinceCreatedMillis = timeSinceCreatedMillis;
+        mInputMediaItemInfos = inputMediaItemInfos;
+        mOutputMediaItemInfo = outputMediaItemInfo;
+        mOperationTypes = operationTypes;
         mMetricsBundle = extras.deepCopy();
     }
 
@@ -194,6 +249,23 @@
         return mTimeSinceCreatedMillis;
     }
 
+    /** Gets information about the input media items, or an empty list if unspecified. */
+    @NonNull
+    public List<MediaItemInfo> getInputMediaItemInfos() {
+        return new ArrayList<>(mInputMediaItemInfos);
+    }
+
+    /** Gets information about the output media item, or {@code null} if unspecified. */
+    @Nullable
+    public MediaItemInfo getOutputMediaItemInfo() {
+        return mOutputMediaItemInfo;
+    }
+
+    /** Gets a set of flags describing the types of operations performed. */
+    public @OperationType long getOperationTypes() {
+        return mOperationTypes;
+    }
+
     /**
      * Gets metrics-related information that is not supported by dedicated methods.
      *
@@ -208,7 +280,7 @@
     @Override
     @NonNull
     public String toString() {
-        return "PlaybackErrorEvent { "
+        return "EditingEndedEvent { "
                 + "finalState = "
                 + mFinalState
                 + ", "
@@ -217,6 +289,15 @@
                 + ", "
                 + "timeSinceCreatedMillis = "
                 + mTimeSinceCreatedMillis
+                + ", "
+                + "inputMediaItemInfos = "
+                + mInputMediaItemInfos
+                + ", "
+                + "outputMediaItemInfo = "
+                + mOutputMediaItemInfo
+                + ", "
+                + "operationTypes = "
+                + mOperationTypes
                 + " }";
     }
 
@@ -227,12 +308,21 @@
         EditingEndedEvent that = (EditingEndedEvent) o;
         return mFinalState == that.mFinalState
                 && mErrorCode == that.mErrorCode
+                && Objects.equals(mInputMediaItemInfos, that.mInputMediaItemInfos)
+                && Objects.equals(mOutputMediaItemInfo, that.mOutputMediaItemInfo)
+                && mOperationTypes == that.mOperationTypes
                 && mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mFinalState, mErrorCode, mTimeSinceCreatedMillis);
+        return Objects.hash(
+                mFinalState,
+                mErrorCode,
+                mInputMediaItemInfos,
+                mOutputMediaItemInfo,
+                mOperationTypes,
+                mTimeSinceCreatedMillis);
     }
 
     @Override
@@ -240,6 +330,9 @@
         dest.writeInt(mFinalState);
         dest.writeInt(mErrorCode);
         dest.writeLong(mTimeSinceCreatedMillis);
+        dest.writeTypedList(mInputMediaItemInfos);
+        dest.writeTypedObject(mOutputMediaItemInfo, /* parcelableFlags= */ 0);
+        dest.writeLong(mOperationTypes);
         dest.writeBundle(mMetricsBundle);
     }
 
@@ -249,15 +342,14 @@
     }
 
     private EditingEndedEvent(@NonNull Parcel in) {
-        int finalState = in.readInt();
-        int errorCode = in.readInt();
-        long timeSinceCreatedMillis = in.readLong();
-        Bundle metricsBundle = in.readBundle();
-
-        mFinalState = finalState;
-        mErrorCode = errorCode;
-        mTimeSinceCreatedMillis = timeSinceCreatedMillis;
-        mMetricsBundle = metricsBundle;
+        mFinalState = in.readInt();
+        mErrorCode = in.readInt();
+        mTimeSinceCreatedMillis = in.readLong();
+        mInputMediaItemInfos = new ArrayList<>();
+        in.readTypedList(mInputMediaItemInfos, MediaItemInfo.CREATOR);
+        mOutputMediaItemInfo = in.readTypedObject(MediaItemInfo.CREATOR);
+        mOperationTypes = in.readLong();
+        mMetricsBundle = in.readBundle();
     }
 
     public static final @NonNull Creator<EditingEndedEvent> CREATOR =
@@ -277,8 +369,11 @@
     @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
     public static final class Builder {
         private final @FinalState int mFinalState;
+        private final ArrayList<MediaItemInfo> mInputMediaItemInfos;
         private @ErrorCode int mErrorCode;
         private long mTimeSinceCreatedMillis;
+        @Nullable private MediaItemInfo mOutputMediaItemInfo;
+        private @OperationType long mOperationTypes;
         private Bundle mMetricsBundle;
 
         /**
@@ -290,6 +385,7 @@
             mFinalState = finalState;
             mErrorCode = ERROR_CODE_NONE;
             mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN;
+            mInputMediaItemInfos = new ArrayList<>();
             mMetricsBundle = new Bundle();
         }
 
@@ -312,20 +408,49 @@
             return this;
         }
 
+        /** Adds information about a media item that was input to the editing operation. */
+        public @NonNull Builder addInputMediaItemInfo(@NonNull MediaItemInfo mediaItemInfo) {
+            mInputMediaItemInfos.add(Objects.requireNonNull(mediaItemInfo));
+            return this;
+        }
+
+        /** Sets information about the output media item. */
+        public @NonNull Builder setOutputMediaItemInfo(@NonNull MediaItemInfo mediaItemInfo) {
+            mOutputMediaItemInfo = Objects.requireNonNull(mediaItemInfo);
+            return this;
+        }
+
+        /**
+         * Adds an operation type to the set of operations performed.
+         *
+         * @param operationType A type of operation performed as part of this editing operation.
+         */
+        public @NonNull Builder addOperationType(@OperationType long operationType) {
+            mOperationTypes |= operationType;
+            return this;
+        }
+
         /**
          * Sets metrics-related information that is not supported by dedicated methods.
          *
          * <p>Used for backwards compatibility by the metrics infrastructure.
          */
         public @NonNull Builder setMetricsBundle(@NonNull Bundle metricsBundle) {
-            mMetricsBundle = metricsBundle;
+            mMetricsBundle = Objects.requireNonNull(metricsBundle);
             return this;
         }
 
         /** Builds an instance. */
         public @NonNull EditingEndedEvent build() {
             return new EditingEndedEvent(
-                    mFinalState, mErrorCode, mTimeSinceCreatedMillis, mMetricsBundle);
+                    mFinalState,
+                    mErrorCode,
+                    mTimeSinceCreatedMillis,
+                    mInputMediaItemInfos,
+                    mOutputMediaItemInfo,
+                    mOperationTypes,
+                    mMetricsBundle);
         }
     }
+
 }
diff --git a/media/java/android/media/metrics/MediaItemInfo.java b/media/java/android/media/metrics/MediaItemInfo.java
new file mode 100644
index 0000000..63dd3cc
--- /dev/null
+++ b/media/java/android/media/metrics/MediaItemInfo.java
@@ -0,0 +1,565 @@
+/*
+ * 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.media.metrics;
+
+import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING;
+
+import android.annotation.FlaggedApi;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.hardware.DataSpace;
+import android.media.MediaCodec;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Size;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** Represents information about a piece of media (for example, an audio or video file). */
+@FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+public final class MediaItemInfo implements Parcelable {
+
+    /** @hide */
+    @IntDef(
+            prefix = {"SOURCE_TYPE_"},
+            value = {
+                SOURCE_TYPE_UNSPECIFIED,
+                SOURCE_TYPE_GALLERY,
+                SOURCE_TYPE_CAMERA,
+                SOURCE_TYPE_EDITING_SESSION,
+                SOURCE_TYPE_LOCAL_FILE,
+                SOURCE_TYPE_REMOTE_FILE,
+                SOURCE_TYPE_REMOTE_LIVE_STREAM,
+                SOURCE_TYPE_GENERATED,
+            })
+    @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    public @interface SourceType {}
+
+    /** The media item's source is not known. */
+    public static final int SOURCE_TYPE_UNSPECIFIED = 0;
+
+    /** The media item came from the device gallery. */
+    public static final int SOURCE_TYPE_GALLERY = 1;
+
+    /** The media item came directly from camera capture. */
+    public static final int SOURCE_TYPE_CAMERA = 2;
+
+    /** The media item was output by a previous editing session. */
+    public static final int SOURCE_TYPE_EDITING_SESSION = 3;
+
+    /** The media item is stored on the local device's file system. */
+    public static final int SOURCE_TYPE_LOCAL_FILE = 4;
+
+    /** The media item is a remote file (for example, it's loaded from an HTTP server). */
+    public static final int SOURCE_TYPE_REMOTE_FILE = 5;
+
+    /** The media item is a remotely-served live stream. */
+    public static final int SOURCE_TYPE_REMOTE_LIVE_STREAM = 6;
+
+    /** The media item was generated by another system. */
+    public static final int SOURCE_TYPE_GENERATED = 7;
+
+    /** @hide */
+    @LongDef(
+            prefix = {"DATA_TYPE_"},
+            flag = true,
+            value = {
+                DATA_TYPE_IMAGE,
+                DATA_TYPE_VIDEO,
+                DATA_TYPE_AUDIO,
+                DATA_TYPE_METADATA,
+                DATA_TYPE_DEPTH,
+                DATA_TYPE_GAIN_MAP,
+                DATA_TYPE_HIGH_FRAME_RATE,
+                DATA_TYPE_CUE_POINTS,
+                DATA_TYPE_GAPLESS,
+                DATA_TYPE_SPATIAL_AUDIO,
+                DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO,
+            })
+    @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    public @interface DataType {}
+
+    /** The media item includes image data. */
+    public static final long DATA_TYPE_IMAGE = 1L;
+
+    /** The media item includes video data. */
+    public static final long DATA_TYPE_VIDEO = 1L << 1;
+
+    /** The media item includes audio data. */
+    public static final long DATA_TYPE_AUDIO = 1L << 2;
+
+    /** The media item includes metadata. */
+    public static final long DATA_TYPE_METADATA = 1L << 3;
+
+    /** The media item includes depth (z-distance) information. */
+    public static final long DATA_TYPE_DEPTH = 1L << 4;
+
+    /** The media item includes gain map information (for example, an Ultra HDR gain map). */
+    public static final long DATA_TYPE_GAIN_MAP = 1L << 5;
+
+    /** The media item includes high frame rate video data. */
+    public static final long DATA_TYPE_HIGH_FRAME_RATE = 1L << 6;
+
+    /** The media item includes time-dependent speed setting metadata. */
+    public static final long DATA_TYPE_CUE_POINTS = 1L << 7;
+
+    /** The media item includes gapless audio metadata. */
+    public static final long DATA_TYPE_GAPLESS = 1L << 8;
+
+    /** The media item includes spatial audio data. */
+    public static final long DATA_TYPE_SPATIAL_AUDIO = 1L << 9;
+
+    /** The media item includes high dynamic range (HDR) video. */
+    public static final long DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO = 1L << 10;
+
+    /** Special value for numerical fields where the value was not specified. */
+    public static final int VALUE_UNSPECIFIED = -1;
+
+    private final @SourceType int mSourceType;
+    private final @DataType long mDataTypes;
+    private final long mDurationMillis;
+    private final long mClipDurationMillis;
+    @Nullable private final String mContainerMimeType;
+    private final List<String> mSampleMimeTypes;
+    private final List<String> mCodecNames;
+    private final int mAudioSampleRateHz;
+    private final int mAudioChannelCount;
+    private final long mAudioSampleCount;
+    private final Size mVideoSize;
+    private final int mVideoDataSpace;
+    private final float mVideoFrameRate;
+    private final long mVideoSampleCount;
+
+    private MediaItemInfo(
+            @SourceType int sourceType,
+            @DataType long dataTypes,
+            long durationMillis,
+            long clipDurationMillis,
+            @Nullable String containerMimeType,
+            List<String> sampleMimeTypes,
+            List<String> codecNames,
+            int audioSampleRateHz,
+            int audioChannelCount,
+            long audioSampleCount,
+            Size videoSize,
+            int videoDataSpace,
+            float videoFrameRate,
+            long videoSampleCount) {
+        mSourceType = sourceType;
+        mDataTypes = dataTypes;
+        mDurationMillis = durationMillis;
+        mClipDurationMillis = clipDurationMillis;
+        mContainerMimeType = containerMimeType;
+        mSampleMimeTypes = sampleMimeTypes;
+        mCodecNames = codecNames;
+        mAudioSampleRateHz = audioSampleRateHz;
+        mAudioChannelCount = audioChannelCount;
+        mAudioSampleCount = audioSampleCount;
+        mVideoSize = videoSize;
+        mVideoDataSpace = videoDataSpace;
+        mVideoFrameRate = videoFrameRate;
+        mVideoSampleCount = videoSampleCount;
+    }
+
+    /**
+     * Returns where the media item came from, or {@link #SOURCE_TYPE_UNSPECIFIED} if not specified.
+     */
+    public @SourceType int getSourceType() {
+        return mSourceType;
+    }
+
+    /** Returns the data types that are present in the media item. */
+    public @DataType long getDataTypes() {
+        return mDataTypes;
+    }
+
+    /**
+     * Returns the duration of the media item, in milliseconds, or {@link #VALUE_UNSPECIFIED} if not
+     * specified.
+     */
+    public long getDurationMillis() {
+        return mDurationMillis;
+    }
+
+    /**
+     * Returns the duration of the clip taken from the media item, in milliseconds, or {@link
+     * #VALUE_UNSPECIFIED} if not specified.
+     */
+    public long getClipDurationMillis() {
+        return mClipDurationMillis;
+    }
+
+    /** Returns the MIME type of the media container, or {@code null} if unspecified. */
+    @Nullable
+    public String getContainerMimeType() {
+        return mContainerMimeType;
+    }
+
+    /**
+     * Returns the MIME types of samples stored in the media container, or an empty list if not
+     * known.
+     */
+    @NonNull
+    public List<String> getSampleMimeTypes() {
+        return new ArrayList<>(mSampleMimeTypes);
+    }
+
+    /**
+     * Returns the {@linkplain MediaCodec#getName() media codec names} for codecs that were used as
+     * part of encoding/decoding this media item, or an empty list if not known or not applicable.
+     */
+    @NonNull
+    public List<String> getCodecNames() {
+        return new ArrayList<>(mCodecNames);
+    }
+
+    /**
+     * Returns the sample rate of audio, in Hertz, or {@link #VALUE_UNSPECIFIED} if not specified.
+     */
+    public int getAudioSampleRateHz() {
+        return mAudioSampleRateHz;
+    }
+
+    /** Returns the number of audio channels, or {@link #VALUE_UNSPECIFIED} if not specified. */
+    public int getAudioChannelCount() {
+        return mAudioChannelCount;
+    }
+
+    /**
+     * Returns the number of audio frames in the item, after clipping (if applicable), or {@link
+     * #VALUE_UNSPECIFIED} if not specified.
+     */
+    public long getAudioSampleCount() {
+        return mAudioSampleCount;
+    }
+
+    /**
+     * Returns the video size, in pixels, or a {@link Size} with width and height set to {@link
+     * #VALUE_UNSPECIFIED} if not specified.
+     */
+    @NonNull
+    public Size getVideoSize() {
+        return mVideoSize;
+    }
+
+    /** Returns the {@linkplain DataSpace data space} for video, as a packed integer. */
+    @SuppressLint("MethodNameUnits") // Packed integer for an android.hardware.DataSpace.
+    public int getVideoDataSpace() {
+        return mVideoDataSpace;
+    }
+
+    /**
+     * Returns the average video frame rate, in frames per second, or {@link #VALUE_UNSPECIFIED} if
+     * not specified.
+     */
+    public float getVideoFrameRate() {
+        return mVideoFrameRate;
+    }
+
+    /**
+     * Returns the number of video frames, aftrer clipping (if applicable), or {@link
+     * #VALUE_UNSPECIFIED} if not specified.
+     */
+    public long getVideoSampleCount() {
+        return mVideoSampleCount;
+    }
+
+    /** Builder for {@link MediaItemInfo}. */
+    @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+    public static final class Builder {
+
+        private @SourceType int mSourceType;
+        private @DataType long mDataTypes;
+        private long mDurationMillis;
+        private long mClipDurationMillis;
+        @Nullable private String mContainerMimeType;
+        private final ArrayList<String> mSampleMimeTypes;
+        private final ArrayList<String> mCodecNames;
+        private int mAudioSampleRateHz;
+        private int mAudioChannelCount;
+        private long mAudioSampleCount;
+        @Nullable private Size mVideoSize;
+        private int mVideoDataSpace;
+        private float mVideoFrameRate;
+        private long mVideoSampleCount;
+
+        /** Creates a new builder. */
+        public Builder() {
+            mSourceType = SOURCE_TYPE_UNSPECIFIED;
+            mDurationMillis = VALUE_UNSPECIFIED;
+            mClipDurationMillis = VALUE_UNSPECIFIED;
+            mSampleMimeTypes = new ArrayList<>();
+            mCodecNames = new ArrayList<>();
+            mAudioSampleRateHz = VALUE_UNSPECIFIED;
+            mAudioChannelCount = VALUE_UNSPECIFIED;
+            mAudioSampleCount = VALUE_UNSPECIFIED;
+            mVideoSize = new Size(VALUE_UNSPECIFIED, VALUE_UNSPECIFIED);
+            mVideoFrameRate = VALUE_UNSPECIFIED;
+            mVideoSampleCount = VALUE_UNSPECIFIED;
+        }
+
+        /** Sets where the media item came from. */
+        public @NonNull Builder setSourceType(@SourceType int sourceType) {
+            mSourceType = sourceType;
+            return this;
+        }
+
+        /** Adds an additional data type represented as part of the media item. */
+        public @NonNull Builder addDataType(@DataType long dataType) {
+            mDataTypes |= dataType;
+            return this;
+        }
+
+        /** Sets the duration of the media item, in milliseconds. */
+        public @NonNull Builder setDurationMillis(long durationMillis) {
+            mDurationMillis = durationMillis;
+            return this;
+        }
+
+        /** Sets the duration of the clip taken from the media item, in milliseconds. */
+        public @NonNull Builder setClipDurationMillis(long clipDurationMillis) {
+            mClipDurationMillis = clipDurationMillis;
+            return this;
+        }
+
+        /** Sets the MIME type of the media container. */
+        public @NonNull Builder setContainerMimeType(@NonNull String containerMimeType) {
+            mContainerMimeType = Objects.requireNonNull(containerMimeType);
+            return this;
+        }
+
+        /** Adds a sample MIME type stored in the media container. */
+        public @NonNull Builder addSampleMimeType(@NonNull String mimeType) {
+            mSampleMimeTypes.add(Objects.requireNonNull(mimeType));
+            return this;
+        }
+
+        /**
+         * Adds an {@linkplain MediaCodec#getName() media codec name} that was used as part of
+         * decoding/encoding this media item.
+         */
+        public @NonNull Builder addCodecName(@NonNull String codecName) {
+            mCodecNames.add(Objects.requireNonNull(codecName));
+            return this;
+        }
+
+        /** Sets the sample rate of audio, in Hertz. */
+        public @NonNull Builder setAudioSampleRateHz(@IntRange(from = 0) int audioSampleRateHz) {
+            mAudioSampleRateHz = audioSampleRateHz;
+            return this;
+        }
+
+        /** Sets the number of audio channels. */
+        public @NonNull Builder setAudioChannelCount(@IntRange(from = 0) int audioChannelCount) {
+            mAudioChannelCount = audioChannelCount;
+            return this;
+        }
+
+        /** Sets the number of audio frames in the item, after clipping (if applicable). */
+        public @NonNull Builder setAudioSampleCount(@IntRange(from = 0) long audioSampleCount) {
+            mAudioSampleCount = audioSampleCount;
+            return this;
+        }
+
+        /** Sets the video size, in pixels. */
+        public @NonNull Builder setVideoSize(@NonNull Size videoSize) {
+            mVideoSize = Objects.requireNonNull(videoSize);
+            return this;
+        }
+
+        /**
+         * Sets the {@link DataSpace} of video frames.
+         *
+         * @param videoDataSpace The data space, returned by {@link DataSpace#pack(int, int, int)}.
+         */
+        public @NonNull Builder setVideoDataSpace(int videoDataSpace) {
+            mVideoDataSpace = videoDataSpace;
+            return this;
+        }
+
+        /** Sets the average video frame rate, in frames per second. */
+        public @NonNull Builder setVideoFrameRate(@FloatRange(from = 0) float videoFrameRate) {
+            mVideoFrameRate = videoFrameRate;
+            return this;
+        }
+
+        /** Sets the number of video frames, after clipping (if applicable). */
+        public @NonNull Builder setVideoSampleCount(@IntRange(from = 0) long videoSampleCount) {
+            mVideoSampleCount = videoSampleCount;
+            return this;
+        }
+
+        /** Builds an instance. */
+        @NonNull
+        public MediaItemInfo build() {
+            return new MediaItemInfo(
+                    mSourceType,
+                    mDataTypes,
+                    mDurationMillis,
+                    mClipDurationMillis,
+                    mContainerMimeType,
+                    mSampleMimeTypes,
+                    mCodecNames,
+                    mAudioSampleRateHz,
+                    mAudioChannelCount,
+                    mAudioSampleCount,
+                    mVideoSize,
+                    mVideoDataSpace,
+                    mVideoFrameRate,
+                    mVideoSampleCount);
+        }
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "MediaItemInfo { "
+                + "sourceType = "
+                + mSourceType
+                + ", "
+                + "dataTypes = "
+                + mDataTypes
+                + ", "
+                + "durationMillis = "
+                + mDurationMillis
+                + ", "
+                + "clipDurationMillis = "
+                + mClipDurationMillis
+                + ", "
+                + "containerMimeType = "
+                + mContainerMimeType
+                + ", "
+                + "sampleMimeTypes = "
+                + mSampleMimeTypes
+                + ", "
+                + "codecNames = "
+                + mCodecNames
+                + ", "
+                + "audioSampleRateHz = "
+                + mAudioSampleRateHz
+                + ", "
+                + "audioChannelCount = "
+                + mAudioChannelCount
+                + ", "
+                + "audioSampleCount = "
+                + mAudioSampleCount
+                + ", "
+                + "videoSize = "
+                + mVideoSize
+                + ", "
+                + "videoDataSpace = "
+                + mVideoDataSpace
+                + ", "
+                + "videoFrameRate = "
+                + mVideoFrameRate
+                + ", "
+                + "videoSampleCount = "
+                + mVideoSampleCount
+                + " }";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        MediaItemInfo that = (MediaItemInfo) o;
+        return mSourceType == that.mSourceType
+                && mDataTypes == that.mDataTypes
+                && mDurationMillis == that.mDurationMillis
+                && mClipDurationMillis == that.mClipDurationMillis
+                && Objects.equals(mContainerMimeType, that.mContainerMimeType)
+                && mSampleMimeTypes.equals(that.mSampleMimeTypes)
+                && mCodecNames.equals(that.mCodecNames)
+                && mAudioSampleRateHz == that.mAudioSampleRateHz
+                && mAudioChannelCount == that.mAudioChannelCount
+                && mAudioSampleCount == that.mAudioSampleCount
+                && Objects.equals(mVideoSize, that.mVideoSize)
+                && Objects.equals(mVideoDataSpace, that.mVideoDataSpace)
+                && mVideoFrameRate == that.mVideoFrameRate
+                && mVideoSampleCount == that.mVideoSampleCount;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mSourceType, mDataTypes);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mSourceType);
+        dest.writeLong(mDataTypes);
+        dest.writeLong(mDurationMillis);
+        dest.writeLong(mClipDurationMillis);
+        dest.writeString(mContainerMimeType);
+        dest.writeStringList(mSampleMimeTypes);
+        dest.writeStringList(mCodecNames);
+        dest.writeInt(mAudioSampleRateHz);
+        dest.writeInt(mAudioChannelCount);
+        dest.writeLong(mAudioSampleCount);
+        dest.writeInt(mVideoSize.getWidth());
+        dest.writeInt(mVideoSize.getHeight());
+        dest.writeInt(mVideoDataSpace);
+        dest.writeFloat(mVideoFrameRate);
+        dest.writeLong(mVideoSampleCount);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    private MediaItemInfo(@NonNull Parcel in) {
+        mSourceType = in.readInt();
+        mDataTypes = in.readLong();
+        mDurationMillis = in.readLong();
+        mClipDurationMillis = in.readLong();
+        mContainerMimeType = in.readString();
+        mSampleMimeTypes = new ArrayList<>();
+        in.readStringList(mSampleMimeTypes);
+        mCodecNames = new ArrayList<>();
+        in.readStringList(mCodecNames);
+        mAudioSampleRateHz = in.readInt();
+        mAudioChannelCount = in.readInt();
+        mAudioSampleCount = in.readLong();
+        int videoSizeWidth = in.readInt();
+        int videoSizeHeight = in.readInt();
+        mVideoSize = new Size(videoSizeWidth, videoSizeHeight);
+        mVideoDataSpace = in.readInt();
+        mVideoFrameRate = in.readFloat();
+        mVideoSampleCount = in.readLong();
+    }
+
+    public static final @NonNull Creator<MediaItemInfo> CREATOR =
+            new Creator<>() {
+                @Override
+                public MediaItemInfo[] newArray(int size) {
+                    return new MediaItemInfo[size];
+                }
+
+                @Override
+                public MediaItemInfo createFromParcel(@NonNull Parcel in) {
+                    return new MediaItemInfo(in);
+                }
+            };
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
index 7cc95c5..0fc1845 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
@@ -36,6 +36,7 @@
 import android.graphics.drawable.Icon
 import android.os.Build
 import android.os.Bundle
+import android.os.Flags
 import android.os.Process
 import android.os.UserHandle
 import android.os.UserManager
@@ -97,16 +98,17 @@
             }
         }
 
-        if (getMaxTargetSdkVersionForUid(context, callingUid) >= Build.VERSION_CODES.P
-            && !isPermissionGranted(
+        if (getMaxTargetSdkVersionForUid(context, callingUid) >= Build.VERSION_CODES.P &&
+            !isPermissionGranted(
                 context, Manifest.permission.REQUEST_DELETE_PACKAGES, callingUid
-            )
-            && !isPermissionGranted(context, Manifest.permission.DELETE_PACKAGES, callingUid)
+            ) &&
+            !isPermissionGranted(context, Manifest.permission.DELETE_PACKAGES, callingUid)
         ) {
             Log.e(
-                LOG_TAG, "Uid " + callingUid + " does not have "
-                    + Manifest.permission.REQUEST_DELETE_PACKAGES + " or "
-                    + Manifest.permission.DELETE_PACKAGES
+                LOG_TAG,
+                "Uid " + callingUid + " does not have " +
+                    Manifest.permission.REQUEST_DELETE_PACKAGES + " or " +
+                    Manifest.permission.DELETE_PACKAGES
             )
             return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR)
         }
@@ -138,8 +140,9 @@
             val profiles = userManager!!.userProfiles
             if (!profiles.contains(uninstalledUser)) {
                 Log.e(
-                    LOG_TAG, "User " + Process.myUserHandle() + " can't request uninstall "
-                        + "for user " + uninstalledUser
+                    LOG_TAG,
+                    "User " + Process.myUserHandle() + " can't request uninstall " +
+                        "for user " + uninstalledUser
                 )
                 return UninstallAborted(UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED)
             }
@@ -202,9 +205,13 @@
         val isSingleUser = isSingleUser()
 
         if (isUpdate) {
-            messageBuilder.append(context.getString(
-                    if (isSingleUser) R.string.uninstall_update_text
-                    else R.string.uninstall_update_text_multiuser
+            messageBuilder.append(
+                context.getString(
+                    if (isSingleUser) {
+                        R.string.uninstall_update_text
+                    } else {
+                        R.string.uninstall_update_text_multiuser
+                    }
                 )
             )
         } else if (uninstallFromAllUsers && !isSingleUser) {
@@ -214,42 +221,42 @@
             val customUserManager = context.createContextAsUser(uninstalledUser!!, 0)
                 .getSystemService(UserManager::class.java)
             val userName = customUserManager!!.userName
-
-            val uninstalledUserType = getUninstalledUserType(myUserHandle, uninstalledUser!!)
-            val messageString: String
-            when (uninstalledUserType) {
-                UserManager.USER_TYPE_PROFILE_MANAGED -> {
+            var messageString = context.getString(
+                    R.string.uninstall_application_text_user,
+                userName
+            )
+            if (userManager!!.isSameProfileGroup(myUserHandle, uninstalledUser!!)) {
+                if (customUserManager!!.isManagedProfile()) {
                     messageString = context.getString(
-                        R.string.uninstall_application_text_current_user_work_profile, userName
+                            R.string.uninstall_application_text_current_user_work_profile, userName
                     )
-                }
-
-                UserManager.USER_TYPE_PROFILE_CLONE -> {
+                } else if (customUserManager!!.isCloneProfile()){
                     isClonedApp = true
                     messageString = context.getString(
-                        R.string.uninstall_application_text_current_user_clone_profile
+                            R.string.uninstall_application_text_current_user_clone_profile
                     )
-                }
-
-                else -> {
+                } else if (Flags.allowPrivateProfile() && customUserManager!!.isPrivateProfile()) {
+                    // TODO(b/324244123): Get these Strings from a User Property API.
                     messageString = context.getString(
-                        R.string.uninstall_application_text_user, userName
+                            R.string.uninstall_application_text_current_user_private_profile
                     )
                 }
-
             }
             messageBuilder.append(messageString)
         } else if (isCloneProfile(uninstalledUser!!)) {
             isClonedApp = true
-            messageBuilder.append(context.getString(
+            messageBuilder.append(
+                context.getString(
                     R.string.uninstall_application_text_current_user_clone_profile
                 )
             )
-        } else if (myUserHandle == UserHandle.SYSTEM
-            && hasClonedInstance(targetAppInfo!!.packageName)
+        } else if (myUserHandle == UserHandle.SYSTEM &&
+            hasClonedInstance(targetAppInfo!!.packageName)
         ) {
-            messageBuilder.append(context.getString(
-                    R.string.uninstall_application_text_with_clone_instance, targetAppLabel
+            messageBuilder.append(
+                context.getString(
+                    R.string.uninstall_application_text_with_clone_instance,
+                    targetAppLabel
                 )
             )
         } else {
@@ -296,31 +303,6 @@
         return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2)
     }
 
-    /**
-     * Returns the type of the user from where an app is being uninstalled. We are concerned with
-     * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile
-     * belong to the same profile group.
-     */
-    private fun getUninstalledUserType(
-        myUserHandle: UserHandle,
-        uninstalledUserHandle: UserHandle
-    ): String? {
-        if (!userManager!!.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) {
-            return null
-        }
-        val customUserManager = context.createContextAsUser(uninstalledUserHandle, 0)
-            .getSystemService(UserManager::class.java)
-        val userTypes =
-            arrayOf(UserManager.USER_TYPE_PROFILE_MANAGED, UserManager.USER_TYPE_PROFILE_CLONE)
-
-        for (userType in userTypes) {
-            if (customUserManager!!.isUserOfType(userType)) {
-                return userType
-            }
-        }
-        return null
-    }
-
     private fun hasClonedInstance(packageName: String): Boolean {
         // Check if clone user is present on the device.
         var cloneUser: UserHandle? = null
@@ -334,8 +316,8 @@
         }
         // Check if another instance of given package exists in clone user profile.
         return try {
-            cloneUser != null
-                && packageManager.getPackageUidAsUser(
+            cloneUser != null &&
+                packageManager.getPackageUidAsUser(
                 packageName, PackageManager.PackageInfoFlags.of(0), cloneUser.identifier
                 ) > 0
         } catch (e: PackageManager.NameNotFoundException) {
@@ -382,7 +364,9 @@
         val storageStatsManager = context.getSystemService(StorageStatsManager::class.java)
         try {
             val stats = storageStatsManager!!.queryStatsForPackage(
-                packageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user
+                packageManager.getApplicationInfo(pkg, 0).storageUuid,
+                pkg,
+                user
             )
             return stats.getDataBytes()
         } catch (e: Exception) {
@@ -423,17 +407,24 @@
         broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId)
         broadcastIntent.setPackage(context.packageName)
         val pendingIntent = PendingIntent.getBroadcast(
-            context, uninstallId, broadcastIntent,
+            context,
+            uninstallId,
+            broadcastIntent,
             PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
         )
         if (!startUninstall(
-                targetPackageName!!, uninstalledUser!!, pendingIntent, uninstallFromAllUsers,
+                targetPackageName!!,
+                uninstalledUser!!,
+                pendingIntent,
+                uninstallFromAllUsers,
                 keepData
             )
         ) {
             handleUninstallResult(
                 PackageInstaller.STATUS_FAILURE,
-                PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0
+                PackageManager.DELETE_FAILED_INTERNAL_ERROR,
+                null,
+                0
             )
         }
     }
@@ -474,9 +465,14 @@
 
         // Caller did not want the result back. So, we either show a Toast, or a Notification.
         if (status == PackageInstaller.STATUS_SUCCESS) {
-            val statusMessage = if (isClonedApp) context.getString(
-                R.string.uninstall_done_clone_app, targetAppLabel
-            ) else context.getString(R.string.uninstall_done_app, targetAppLabel)
+            val statusMessage = if (isClonedApp) {
+                context.getString(
+                R.string.uninstall_done_clone_app,
+                    targetAppLabel
+            )
+            } else {
+                context.getString(R.string.uninstall_done_app, targetAppLabel)
+            }
             uninstallResult.setValue(
                 UninstallSuccess(activityResultCode = legacyStatus, message = statusMessage)
             )
@@ -499,27 +495,32 @@
                         findUserOfDeviceAdmin(myUserHandle, targetPackageName!!)
                     if (otherBlockingUserHandle == null) {
                         Log.d(
-                            LOG_TAG, "Uninstall failed because $targetPackageName"
-                                + " is a device admin"
+                            LOG_TAG,
+                            "Uninstall failed because $targetPackageName" +
+                                " is a device admin"
                         )
                         addDeviceManagerButton(context, uninstallFailedNotification)
                         setBigText(
-                            uninstallFailedNotification, context.getString(
+                            uninstallFailedNotification,
+                            context.getString(
                                 R.string.uninstall_failed_device_policy_manager
                             )
                         )
                     } else {
                         Log.d(
-                            LOG_TAG, "Uninstall failed because $targetPackageName"
-                                + " is a device admin of user $otherBlockingUserHandle"
+                            LOG_TAG,
+                            "Uninstall failed because $targetPackageName" +
+                                " is a device admin of user $otherBlockingUserHandle"
                         )
                         val userName = context.createContextAsUser(otherBlockingUserHandle, 0)
                             .getSystemService(UserManager::class.java)!!.userName
                         setBigText(
-                            uninstallFailedNotification, String.format(
+                            uninstallFailedNotification,
+                            String.format(
                                 context.getString(
                                     R.string.uninstall_failed_device_policy_manager_of_user
-                                ), userName
+                                ),
+                                userName
                             )
                         )
                     }
@@ -528,7 +529,9 @@
                 PackageManager.DELETE_FAILED_OWNER_BLOCKED -> {
                     val otherBlockingUserHandle = findBlockingUser(targetPackageName!!)
                     val isProfileOfOrSame = isProfileOfOrSame(
-                        userManager!!, myUserHandle, otherBlockingUserHandle
+                        userManager!!,
+                        myUserHandle,
+                        otherBlockingUserHandle
                     )
                     if (isProfileOfOrSame) {
                         addDeviceManagerButton(context, uninstallFailedNotification)
@@ -538,15 +541,19 @@
                     var bigText: String? = null
                     if (otherBlockingUserHandle == null) {
                         Log.d(
-                            LOG_TAG, "Uninstall failed for $targetPackageName " +
+                            LOG_TAG,
+                            "Uninstall failed for $targetPackageName " +
                                 "with code $status no blocking user"
                         )
                     } else if (otherBlockingUserHandle === UserHandle.SYSTEM) {
                         bigText = context.getString(R.string.uninstall_blocked_device_owner)
                     } else {
                         bigText = context.getString(
-                            if (uninstallFromAllUsers) R.string.uninstall_all_blocked_profile_owner
-                            else R.string.uninstall_blocked_profile_owner
+                            if (uninstallFromAllUsers) {
+                                R.string.uninstall_all_blocked_profile_owner
+                            } else {
+                                R.string.uninstall_blocked_profile_owner
+                            }
                         )
                     }
                     bigText?.let { setBigText(uninstallFailedNotification, it) }
@@ -554,8 +561,9 @@
 
                 else -> {
                     Log.d(
-                        LOG_TAG, "Uninstall blocked for $targetPackageName"
-                            + " with legacy code $legacyStatus"
+                        LOG_TAG,
+                        "Uninstall blocked for $targetPackageName" +
+                            " with legacy code $legacyStatus"
                     )
                 }
             }
@@ -639,7 +647,9 @@
                 Icon.createWithResource(context, R.drawable.ic_settings_multiuser),
                 context.getString(R.string.manage_users),
                 PendingIntent.getActivity(
-                    context, 0, getUserSettingsIntent(),
+                    context,
+                    0,
+                    getUserSettingsIntent(),
                     PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                 )
             )
@@ -668,7 +678,9 @@
                 Icon.createWithResource(context, R.drawable.ic_lock),
                 context.getString(R.string.manage_device_administrators),
                 PendingIntent.getActivity(
-                    context, 0, getDeviceManagerIntent(),
+                    context,
+                    0,
+                    getDeviceManagerIntent(),
                     PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                 )
             )
@@ -706,7 +718,8 @@
             context.createContextAsUser(targetUser, 0)
                 .packageManager.packageInstaller.uninstall(
                     VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
-                    flags, pendingIntent.intentSender
+                    flags,
+                    pendingIntent.intentSender
                 )
             true
         } catch (e: IllegalArgumentException) {
@@ -719,7 +732,8 @@
         if (callback != null) {
             callback!!.onUninstallComplete(
                 targetPackageName!!,
-                PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user"
+                PackageManager.DELETE_FAILED_ABORTED,
+                "Cancelled by user"
             )
         }
     }
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index bab6781..d622eb8 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -17,3 +17,9 @@
     }
 }
 
+flag {
+   name: "bluetooth_qs_tile_dialog_auto_on_toggle"
+   namespace: "bluetooth"
+   description: "Displays the auto on toggle in the bluetooth QS tile dialog"
+   bug: "316985153"
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index e3012cd..249fa7f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -1621,6 +1621,7 @@
     }
 
     public static class AppEntry extends SizeInfo {
+        @VisibleForTesting String mProfileType;
         @Nullable public final File apkFile;
         public final long id;
         public String label;
@@ -1647,11 +1648,6 @@
          */
         public boolean isHomeApp;
 
-        /**
-         * Whether or not it's a cloned app .
-         */
-        public boolean isCloned;
-
         public String getNormalizedLabel() {
             if (normalizedLabel != null) {
                 return normalizedLabel;
@@ -1692,11 +1688,21 @@
                         () -> this.ensureLabelDescriptionLocked(context));
             }
             UserManager um = UserManager.get(context);
-            this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
             UserInfo userInfo = um.getUserInfo(UserHandle.getUserId(info.uid));
-            if (userInfo != null) {
-                this.isCloned = userInfo.isCloneProfile();
-            }
+            mProfileType = userInfo.userType;
+            this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
+        }
+
+        public boolean isClonedProfile() {
+            return UserManager.USER_TYPE_PROFILE_CLONE.equals(mProfileType);
+        }
+
+        public boolean isManagedProfile() {
+            return UserManager.USER_TYPE_PROFILE_MANAGED.equals(mProfileType);
+        }
+
+        public boolean isPrivateProfile() {
+            return UserManager.USER_TYPE_PROFILE_PRIVATE.equals(mProfileType);
         }
 
         /**
@@ -1890,16 +1896,24 @@
     };
 
     public static final AppFilter FILTER_WORK = new AppFilter() {
-        private int mCurrentUser;
 
         @Override
-        public void init() {
-            mCurrentUser = ActivityManager.getCurrentUser();
-        }
+        public void init() {}
 
         @Override
         public boolean filterApp(AppEntry entry) {
-            return !entry.showInPersonalTab;
+            return !entry.showInPersonalTab && entry.isManagedProfile();
+        }
+    };
+
+    public static final AppFilter FILTER_PRIVATE_PROFILE = new AppFilter() {
+
+        @Override
+        public void init() {}
+
+        @Override
+        public boolean filterApp(AppEntry entry) {
+            return !entry.showInPersonalTab && entry.isPrivateProfile();
         }
     };
 
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 c5598bf..213a66e 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
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.pm.ApplicationInfo;
+import android.os.UserManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -297,11 +298,26 @@
     @Test
     public void testPersonalAndWorkFiltersDisplaysCorrectApps() {
         mEntry.showInPersonalTab = true;
+        mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM;
         assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue();
         assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isFalse();
 
         mEntry.showInPersonalTab = false;
+        mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_MANAGED;
         assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse();
         assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isTrue();
     }
+
+    @Test
+    public void testPrivateProfileFilterDisplaysCorrectApps() {
+        mEntry.showInPersonalTab = true;
+        mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM;
+        assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue();
+        assertThat(ApplicationsState.FILTER_PRIVATE_PROFILE.filterApp(mEntry)).isFalse();
+
+        mEntry.showInPersonalTab = false;
+        mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_PRIVATE;
+        assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse();
+        assertThat(ApplicationsState.FILTER_PRIVATE_PROFILE.filterApp(mEntry)).isTrue();
+    }
 }
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index e55bbec..9ecbd50 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -31,6 +31,7 @@
 import java.io.FileOutputStream;
 import java.io.PrintStream;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 public class SettingsStateTest extends AndroidTestCase {
@@ -626,4 +627,121 @@
             assertEquals(VALUE2, settingsState.getSettingLocked(INVALID_STAGED_FLAG_1).getValue());
         }
     }
+
+    public void testsetSettingsLockedKeepTrunkDefault() throws Exception {
+        final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile));
+        os.print(
+                "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>"
+                        + "<settings version=\"120\">"
+                        + "  <setting id=\"0\" name=\"test_namespace/flag0\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "  <setting id=\"1\" name=\"test_namespace/flag1\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "  <setting id=\"2\" name=\"test_namespace/com.android.flags.flag3\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "  <setting id=\"3\" "
+                        + "name=\"test_another_namespace/com.android.another.flags.flag0\" "
+                            + "value=\"false\" package=\"com.android.another.flags\" />"
+                        + "</settings>");
+        os.close();
+
+        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+
+        SettingsState settingsState = new SettingsState(
+                getContext(), mLock, mSettingsFile, configKey,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+        String prefix = "test_namespace";
+        Map<String, String> keyValues =
+                Map.of("test_namespace/flag0", "true", "test_namespace/flag2", "false");
+        String packageName = "com.android.flags";
+
+        parsed_flags flags = parsed_flags
+                .newBuilder()
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setPackage(packageName)
+                        .setName("flag3")
+                        .setNamespace(prefix)
+                        .setDescription("test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.DISABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .addParsedFlag(parsed_flag
+                    .newBuilder()
+                        .setPackage("com.android.another.flags")
+                        .setName("flag0")
+                        .setNamespace("test_another_namespace")
+                        .setDescription("test flag")
+                        .addBug("12345678")
+                        .setState(Aconfig.flag_state.DISABLED)
+                        .setPermission(Aconfig.flag_permission.READ_WRITE))
+                .build();
+
+        synchronized (mLock) {
+            settingsState.loadAconfigDefaultValues(
+                    flags.toByteArray(), settingsState.getAconfigDefaultValues());
+            List<String> updates =
+                    settingsState.setSettingsLocked("test_namespace/", keyValues, packageName);
+            assertEquals(3, updates.size());
+
+            SettingsState.Setting s;
+
+            s = settingsState.getSettingLocked("test_namespace/flag0");
+            assertEquals("true", s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/flag1");
+            assertNull(s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/flag2");
+            assertEquals("false", s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/com.android.flags.flag3");
+            assertEquals("false", s.getValue());
+
+            s = settingsState.getSettingLocked(
+                    "test_another_namespace/com.android.another.flags.flag0");
+            assertEquals("false", s.getValue());
+        }
+    }
+
+    public void testsetSettingsLockedNoTrunkDefault() throws Exception {
+        final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile));
+        os.print(
+                "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>"
+                        + "<settings version=\"120\">"
+                        + "  <setting id=\"0\" name=\"test_namespace/flag0\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "  <setting id=\"1\" name=\"test_namespace/flag1\" "
+                            + "value=\"false\" package=\"com.android.flags\" />"
+                        + "</settings>");
+        os.close();
+
+        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+
+        SettingsState settingsState = new SettingsState(
+                getContext(), mLock, mSettingsFile, configKey,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+        Map<String, String> keyValues =
+                Map.of("test_namespace/flag0", "true", "test_namespace/flag2", "false");
+        String packageName = "com.android.flags";
+
+        synchronized (mLock) {
+            List<String> updates =
+                    settingsState.setSettingsLocked("test_namespace/", keyValues, packageName);
+            assertEquals(3, updates.size());
+
+            SettingsState.Setting s;
+
+            s = settingsState.getSettingLocked("test_namespace/flag0");
+            assertEquals("true", s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/flag1");
+            assertNull(s.getValue());
+
+            s = settingsState.getSettingLocked("test_namespace/flag2");
+            assertEquals("false", s.getValue());
+        }
+    }
 }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 84ef6e5..926e181 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -917,6 +917,9 @@
     <!-- Permissions required for CTS test - GrammaticalInflectionManagerTest -->
     <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
 
+    <!-- Permission required for CTS test - CtsPackageManagerTestCases-->
+    <uses-permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT" />
+
     <application
         android:label="@string/app_label"
         android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7a4e60a..56576f1 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -354,13 +354,6 @@
 }
 
 flag {
-   name: "bluetooth_qs_tile_dialog_auto_on_toggle"
-   namespace: "systemui"
-   description: "Displays the auto on toggle in the bluetooth QS tile dialog"
-   bug: "316985153"
-}
-
-flag {
    name: "smartspace_relocate_to_bottom"
    namespace: "systemui"
    description: "Relocate Smartspace to bottom of the Lock Screen"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt
new file mode 100644
index 0000000..abe1e3d
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt
@@ -0,0 +1,377 @@
+/*
+ * 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.surfaceeffects.loadingeffect
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.graphics.Paint
+import android.graphics.RenderEffect
+import android.view.View
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
+
+/**
+ * Plays loading effect with the given configuration.
+ *
+ * @param baseType immutable base shader type. This is used for constructing the shader. Reconstruct
+ *   the [LoadingEffect] if the base type needs to be changed.
+ * @param config immutable parameters that are used for drawing the effect.
+ * @param paintCallback triggered every frame when animation is playing. Use this to draw the effect
+ *   with [Canvas.drawPaint].
+ * @param renderEffectCallback triggered every frame when animation is playing. Use this to draw the
+ *   effect with [RenderEffect].
+ * @param animationStateChangedCallback triggered when the [AnimationState] changes. Optional.
+ *
+ * The client is responsible to actually draw the [Paint] or [RenderEffect] returned in the
+ * callback. Note that [View.invalidate] must be called on each callback. There are a few ways to
+ * render the effect:
+ * 1) Use [Canvas.drawPaint]. (Preferred. Significantly cheaper!)
+ * 2) Set [RenderEffect] to the [View]. (Good for chaining effects.)
+ * 3) Use [RenderNode.setRenderEffect]. (This may be least preferred, as 2 should do what you want.)
+ *
+ * <p>First approach is more performant than other ones because [RenderEffect] forces an
+ * intermediate render pass of the View to a texture to feed into it.
+ *
+ * <p>If going with the first approach, your custom [View] would look like as follow:
+ * <pre>{@code
+ *     private var paint: Paint? = null
+ *     // Override [View.onDraw].
+ *     override fun onDraw(canvas: Canvas) {
+ *         // RuntimeShader requires hardwareAcceleration.
+ *         if (!canvas.isHardwareAccelerated) return
+ *
+ *         paint?.let { canvas.drawPaint(it) }
+ *     }
+ *
+ *     // This is called [Callback.onDraw]
+ *     fun draw(paint: Paint) {
+ *         this.paint = paint
+ *
+ *         // Must call invalidate to trigger View#onDraw
+ *         invalidate()
+ *     }
+ * }</pre>
+ *
+ * <p>If going with the second approach, it doesn't require an extra custom [View], and it is as
+ * simple as calling [View.setRenderEffect] followed by [View.invalidate]. You can also chain the
+ * effect with other [RenderEffect].
+ *
+ * <p>Third approach is an option, but it's more of a boilerplate so you would like to stick with
+ * the second option. If you want to go with this option for some reason, below is the example:
+ * <pre>{@code
+ *     // Initialize the shader and paint to use to pass into the [Canvas].
+ *     private val renderNode = RenderNode("LoadingEffect")
+ *
+ *     // Override [View.onDraw].
+ *     override fun onDraw(canvas: Canvas) {
+ *         // RuntimeShader requires hardwareAcceleration.
+ *         if (!canvas.isHardwareAccelerated) return
+ *
+ *         if (renderNode.hasDisplayList()) {
+ *             canvas.drawRenderNode(renderNode)
+ *         }
+ *     }
+ *
+ *     // This is called [Callback.onDraw]
+ *     fun draw(renderEffect: RenderEffect) {
+ *         renderNode.setPosition(0, 0, width, height)
+ *         renderNode.setRenderEffect(renderEffect)
+ *
+ *         val recordingCanvas = renderNode.beginRecording()
+ *         // We need at least 1 drawing instruction.
+ *         recordingCanvas.drawColor(Color.TRANSPARENT)
+ *         renderNode.endRecording()
+ *
+ *         // Must call invalidate to trigger View#onDraw
+ *         invalidate()
+ *     }
+ * }</pre>
+ */
+class LoadingEffect
+private constructor(
+    baseType: TurbulenceNoiseShader.Companion.Type,
+    private val config: TurbulenceNoiseAnimationConfig,
+    private val paintCallback: PaintDrawCallback?,
+    private val renderEffectCallback: RenderEffectDrawCallback?,
+    private val animationStateChangedCallback: AnimationStateChangedCallback? = null
+) {
+    constructor(
+        baseType: TurbulenceNoiseShader.Companion.Type,
+        config: TurbulenceNoiseAnimationConfig,
+        paintCallback: PaintDrawCallback,
+        animationStateChangedCallback: AnimationStateChangedCallback? = null
+    ) : this(
+        baseType,
+        config,
+        paintCallback,
+        renderEffectCallback = null,
+        animationStateChangedCallback
+    )
+    constructor(
+        baseType: TurbulenceNoiseShader.Companion.Type,
+        config: TurbulenceNoiseAnimationConfig,
+        renderEffectCallback: RenderEffectDrawCallback,
+        animationStateChangedCallback: AnimationStateChangedCallback? = null
+    ) : this(
+        baseType,
+        config,
+        paintCallback = null,
+        renderEffectCallback,
+        animationStateChangedCallback
+    )
+
+    private val turbulenceNoiseShader: TurbulenceNoiseShader =
+        TurbulenceNoiseShader(baseType).apply { applyConfig(config) }
+    private var currentAnimator: ValueAnimator? = null
+    private var state: AnimationState = AnimationState.NOT_PLAYING
+        set(value) {
+            if (field != value) {
+                animationStateChangedCallback?.onStateChanged(field, value)
+                field = value
+            }
+        }
+
+    // We create a paint instance only if the client renders it with Paint.
+    private val paint =
+        if (paintCallback != null) {
+            Paint().apply { this.shader = turbulenceNoiseShader }
+        } else {
+            null
+        }
+
+    /** Plays LoadingEffect. */
+    fun play() {
+        if (state != AnimationState.NOT_PLAYING) {
+            return // Ignore if any of the animation is playing.
+        }
+
+        playEaseIn()
+    }
+
+    // TODO(b/237282226): Support force finish.
+    /** Finishes the main animation, which triggers the ease-out animation. */
+    fun finish() {
+        if (state == AnimationState.MAIN) {
+            // Calling Animator#end sets the animation state back to the initial state. Using pause
+            // to avoid visual artifacts.
+            currentAnimator?.pause()
+            currentAnimator = null
+
+            playEaseOut()
+        }
+    }
+
+    /** Updates the noise color dynamically. */
+    fun updateColor(newColor: Int) {
+        turbulenceNoiseShader.setColor(newColor)
+    }
+
+    /**
+     * Retrieves the noise offset x, y, z values. This is useful for replaying the animation
+     * smoothly from the last animation, by passing in the last values to the next animation.
+     */
+    fun getNoiseOffset(): Array<Float> {
+        return arrayOf(
+            turbulenceNoiseShader.noiseOffsetX,
+            turbulenceNoiseShader.noiseOffsetY,
+            turbulenceNoiseShader.noiseOffsetZ
+        )
+    }
+
+    private fun playEaseIn() {
+        if (state != AnimationState.NOT_PLAYING) {
+            return
+        }
+        state = AnimationState.EASE_IN
+
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = config.easeInDuration.toLong()
+
+        // Animation should start from the initial position to avoid abrupt transition.
+        val initialX = turbulenceNoiseShader.noiseOffsetX
+        val initialY = turbulenceNoiseShader.noiseOffsetY
+        val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+        animator.addUpdateListener { updateListener ->
+            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+            val progress = updateListener.animatedValue as Float
+
+            turbulenceNoiseShader.setNoiseMove(
+                initialX + timeInSec * config.noiseMoveSpeedX,
+                initialY + timeInSec * config.noiseMoveSpeedY,
+                initialZ + timeInSec * config.noiseMoveSpeedZ
+            )
+
+            // TODO: Replace it with a better curve.
+            turbulenceNoiseShader.setOpacity(progress * config.luminosityMultiplier)
+
+            draw()
+        }
+
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    currentAnimator = null
+                    playMain()
+                }
+            }
+        )
+
+        animator.start()
+        this.currentAnimator = animator
+    }
+
+    private fun playMain() {
+        if (state != AnimationState.EASE_IN) {
+            return
+        }
+        state = AnimationState.MAIN
+
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = config.maxDuration.toLong()
+
+        // Animation should start from the initial position to avoid abrupt transition.
+        val initialX = turbulenceNoiseShader.noiseOffsetX
+        val initialY = turbulenceNoiseShader.noiseOffsetY
+        val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+        turbulenceNoiseShader.setOpacity(config.luminosityMultiplier)
+
+        animator.addUpdateListener { updateListener ->
+            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+            turbulenceNoiseShader.setNoiseMove(
+                initialX + timeInSec * config.noiseMoveSpeedX,
+                initialY + timeInSec * config.noiseMoveSpeedY,
+                initialZ + timeInSec * config.noiseMoveSpeedZ
+            )
+
+            draw()
+        }
+
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    currentAnimator = null
+                    playEaseOut()
+                }
+            }
+        )
+
+        animator.start()
+        this.currentAnimator = animator
+    }
+
+    private fun playEaseOut() {
+        if (state != AnimationState.MAIN) {
+            return
+        }
+        state = AnimationState.EASE_OUT
+
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = config.easeOutDuration.toLong()
+
+        // Animation should start from the initial position to avoid abrupt transition.
+        val initialX = turbulenceNoiseShader.noiseOffsetX
+        val initialY = turbulenceNoiseShader.noiseOffsetY
+        val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+        animator.addUpdateListener { updateListener ->
+            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+            val progress = updateListener.animatedValue as Float
+
+            turbulenceNoiseShader.setNoiseMove(
+                initialX + timeInSec * config.noiseMoveSpeedX,
+                initialY + timeInSec * config.noiseMoveSpeedY,
+                initialZ + timeInSec * config.noiseMoveSpeedZ
+            )
+
+            // TODO: Replace it with a better curve.
+            turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier)
+
+            draw()
+        }
+
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    currentAnimator = null
+                    state = AnimationState.NOT_PLAYING
+                }
+            }
+        )
+
+        animator.start()
+        this.currentAnimator = animator
+    }
+
+    private fun draw() {
+        paintCallback?.onDraw(paint!!)
+        renderEffectCallback?.onDraw(
+            RenderEffect.createRuntimeShaderEffect(turbulenceNoiseShader, "in_src")
+        )
+    }
+
+    companion object {
+        /**
+         * States of the loading effect animation.
+         *
+         * <p>The state is designed to be follow the order below: [AnimationState.EASE_IN],
+         * [AnimationState.MAIN], [AnimationState.EASE_OUT]. Note that ease in and out don't
+         * necessarily mean the acceleration and deceleration in the animation curve. They simply
+         * mean each stage of the animation. (i.e. Intro, core, and rest)
+         */
+        enum class AnimationState {
+            EASE_IN,
+            MAIN,
+            EASE_OUT,
+            NOT_PLAYING
+        }
+
+        /** Client must implement one of the draw callbacks. */
+        interface PaintDrawCallback {
+            /**
+             * A callback with a [Paint] object that contains shader info, which is triggered every
+             * frame while animation is playing. Note that the [Paint] object here is always the
+             * same instance.
+             */
+            fun onDraw(loadingPaint: Paint)
+        }
+
+        interface RenderEffectDrawCallback {
+            /**
+             * A callback with a [RenderEffect] object that contains shader info, which is triggered
+             * every frame while animation is playing. Note that the [RenderEffect] instance is
+             * different each time to update shader uniforms.
+             */
+            fun onDraw(loadingRenderEffect: RenderEffect)
+        }
+
+        /** Optional callback that is triggered when the animation state changes. */
+        interface AnimationStateChangedCallback {
+            /**
+             * A callback that's triggered when the [AnimationState] changes. Example usage is
+             * performing a cleanup when [AnimationState] becomes [NOT_PLAYING].
+             */
+            fun onStateChanged(oldState: AnimationState, newState: AnimationState) {}
+        }
+
+        private const val MS_TO_SEC = 0.001f
+
+        private val TAG = LoadingEffect::class.java.simpleName
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
index 30108ac..8dd90a8 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
@@ -30,6 +30,7 @@
     companion object {
         private const val UNIFORMS =
             """
+            uniform shader in_src; // Needed to support RenderEffect.
             uniform float in_gridNum;
             uniform vec3 in_noiseMove;
             uniform vec2 in_size;
@@ -114,6 +115,7 @@
         setSize(config.width, config.height)
         setLumaMatteFactors(config.lumaMatteBlendFactor, config.lumaMatteOverallBrightness)
         setInverseNoiseLuminosity(config.shouldInverseNoiseLuminosity)
+        setNoiseMove(config.noiseOffsetX, config.noiseOffsetY, config.noiseOffsetZ)
     }
 
     /** Sets the number of grid for generating noise. */
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 9a34d6f..36ab46b4 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
 import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
@@ -104,7 +105,7 @@
         throwComposeUnavailableError()
     }
 
-    override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View {
+    override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View {
         throwComposeUnavailableError()
     }
 
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 51d2a03..5b6aa09 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -40,6 +40,7 @@
 import com.android.systemui.communal.ui.compose.CommunalContainer
 import com.android.systemui.communal.ui.compose.CommunalHub
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.keyboard.stickykeys.ui.view.createStickyKeyIndicatorView
 import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
@@ -161,7 +162,7 @@
         }
     }
 
-    override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View {
+    override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View {
         return ComposeView(context).apply {
             setContent { PlatformTheme { CommunalContainer(viewModel = viewModel) } }
         }
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 92bc1f1..bc85513 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
@@ -23,7 +23,9 @@
 import com.android.compose.animation.scene.updateSceneTransitionLayoutState
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import com.android.systemui.communal.ui.compose.extensions.allowGestures
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.transform
 
@@ -51,7 +53,7 @@
 @Composable
 fun CommunalContainer(
     modifier: Modifier = Modifier,
-    viewModel: BaseCommunalViewModel,
+    viewModel: CommunalViewModel,
 ) {
     val currentScene: SceneKey by
         viewModel.currentScene
@@ -63,6 +65,7 @@
             onChangeScene = { viewModel.onSceneChanged(it.toCommunalSceneKey()) },
             transitions = sceneTransitions,
         )
+    val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false)
 
     // 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.
@@ -75,7 +78,7 @@
 
     SceneTransitionLayout(
         state = sceneTransitionLayoutState,
-        modifier = modifier.fillMaxSize(),
+        modifier = modifier.fillMaxSize().allowGestures(allowed = touchesAllowed),
         swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize),
     ) {
         scene(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index f70b6a5..b299ca7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
 import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
 import com.android.systemui.testKosmos
@@ -102,6 +103,7 @@
                 testScope,
                 kosmos.communalInteractor,
                 kosmos.communalTutorialInteractor,
+                kosmos.shadeInteractor,
                 mediaHost,
                 logcatLogBuffer("CommunalViewModelTest"),
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index c23ec22..bf1d76f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -74,6 +74,8 @@
     private val dozeParameters = kosmos.dozeParameters
     private val underTest by lazy { kosmos.keyguardRootViewModel }
 
+    private val viewState = ViewStateAccessor()
+
     @Before
     fun setUp() {
         mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
@@ -251,7 +253,7 @@
     @Test
     fun alpha_idleOnHub_isZero() =
         testScope.runTest {
-            val alpha by collectLastValue(underTest.alpha)
+            val alpha by collectLastValue(underTest.alpha(viewState))
 
             // Hub transition state is idle with hub open.
             communalRepository.setTransitionState(
@@ -269,7 +271,7 @@
     @Test
     fun alpha_transitionToHub_isZero() =
         testScope.runTest {
-            val alpha by collectLastValue(underTest.alpha)
+            val alpha by collectLastValue(underTest.alpha(viewState))
 
             keyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
@@ -283,7 +285,7 @@
     @Test
     fun alpha_transitionFromHubToLockscreen_isOne() =
         testScope.runTest {
-            val alpha by collectLastValue(underTest.alpha)
+            val alpha by collectLastValue(underTest.alpha(viewState))
 
             // Transition to the glanceable hub and back.
             keyguardTransitionRepository.sendTransitionSteps(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index 0c7ce97..288c083 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
+import android.os.Build
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.assertLogsWtf
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -30,7 +32,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
-import org.junit.Assert.assertThrows
+import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -116,9 +118,9 @@
         underTest.setGroupExpanded(summary1, false)
 
         // Expanding again should throw.
-        assertThrows(IllegalArgumentException::class.java) {
-            underTest.setGroupExpanded(summary1, true)
-        }
+        // TODO(b/320238410): Remove this check when robolectric supports wtf assertions.
+        Assume.assumeFalse(Build.FINGERPRINT.contains("robolectric"))
+        assertLogsWtf { underTest.setGroupExpanded(summary1, true) }
     }
 
     @Test
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
index a0f916c..ac781ec 100644
--- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -81,7 +81,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="21dp"
-        android:minHeight="145dp"
+        android:minHeight="@dimen/bluetooth_dialog_scroll_view_min_height"
         android:fillViewport="true"
         app:layout_constrainedHeight="true"
         app:layout_constraintStart_toStartOf="parent"
@@ -97,11 +97,11 @@
             <TextView
                 android:id="@+id/bluetooth_toggle_title"
                 android:layout_width="0dp"
-                android:layout_height="64dp"
-                android:maxLines="1"
+                android:layout_height="68dp"
+                android:maxLines="2"
                 android:ellipsize="end"
                 android:gravity="start|center_vertical"
-                android:paddingEnd="0dp"
+                android:paddingEnd="15dp"
                 android:paddingStart="36dp"
                 android:text="@string/turn_on_bluetooth"
                 android:clickable="false"
@@ -114,7 +114,7 @@
             <Switch
                 android:id="@+id/bluetooth_toggle"
                 android:layout_width="wrap_content"
-                android:layout_height="64dp"
+                android:layout_height="68dp"
                 android:gravity="start|center_vertical"
                 android:paddingEnd="40dp"
                 android:contentDescription="@string/turn_on_bluetooth"
@@ -126,14 +126,79 @@
                 app:layout_constraintStart_toEndOf="@+id/bluetooth_toggle_title"
                 app:layout_constraintTop_toTopOf="parent" />
 
+            <androidx.constraintlayout.widget.Group
+                android:id="@+id/bluetooth_auto_on_toggle_layout"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:visibility="gone"
+                app:constraint_referenced_ids="bluetooth_auto_on_toggle_title,bluetooth_auto_on_toggle,bluetooth_auto_on_toggle_info_icon,bluetooth_auto_on_toggle_info_text" />
+
+            <TextView
+                android:id="@+id/bluetooth_auto_on_toggle_title"
+                android:layout_width="0dp"
+                android:layout_height="68dp"
+                android:layout_marginBottom="20dp"
+                android:maxLines="2"
+                android:ellipsize="end"
+                android:text="@string/turn_on_bluetooth_auto_tomorrow"
+                android:gravity="start|center_vertical"
+                android:paddingEnd="15dp"
+                android:paddingStart="36dp"
+                android:clickable="false"
+                android:textAppearance="@style/TextAppearance.Dialog.Title"
+                android:textSize="16sp"
+                app:layout_constraintEnd_toStartOf="@+id/bluetooth_auto_on_toggle"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle_title" />
+
+            <Switch
+                android:id="@+id/bluetooth_auto_on_toggle"
+                android:layout_width="wrap_content"
+                android:layout_height="68dp"
+                android:layout_marginBottom="20dp"
+                android:gravity="start|center_vertical"
+                android:paddingEnd="40dp"
+                android:contentDescription="@string/turn_on_bluetooth_auto_tomorrow"
+                android:switchMinWidth="@dimen/settingslib_switch_track_width"
+                android:theme="@style/MainSwitch.Settingslib"
+                android:thumb="@drawable/settingslib_thumb_selector"
+                android:track="@drawable/settingslib_track_selector"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/bluetooth_auto_on_toggle_title"
+                app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" />
+
+            <ImageView
+                android:id="@+id/bluetooth_auto_on_toggle_info_icon"
+                android:src="@drawable/ic_info_outline"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:tint="?android:attr/textColorTertiary"
+                android:paddingStart="36dp"
+                android:layout_marginTop="20dp"
+                android:layout_marginBottom="@dimen/bluetooth_dialog_layout_margin"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/bluetooth_auto_on_toggle" />
+
+            <TextView
+                android:id="@+id/bluetooth_auto_on_toggle_info_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="20dp"
+                android:paddingStart="36dp"
+                android:paddingEnd="40dp"
+                android:text="@string/turn_on_bluetooth_auto_info"
+                android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/bluetooth_auto_on_toggle_info_icon" />
+
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/device_list"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle"
-                app:layout_constraintBottom_toTopOf="@+id/see_all_button" />
+                app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" />
 
             <Button
                 android:id="@+id/see_all_button"
@@ -168,12 +233,10 @@
                 android:background="@drawable/bluetooth_tile_dialog_bg_off"
                 android:layout_width="0dp"
                 android:layout_height="64dp"
-                android:layout_marginBottom="9dp"
                 android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/see_all_button"
-                app:layout_constraintBottom_toTopOf="@+id/done_button"
                 android:drawableStart="@drawable/ic_add"
                 android:drawablePadding="20dp"
                 android:drawableTint="?android:attr/textColorPrimary"
@@ -186,11 +249,19 @@
                 android:ellipsize="end"
                 android:visibility="gone" />
 
+            <androidx.constraintlayout.widget.Barrier
+                android:id="@+id/barrier"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                app:barrierDirection="bottom"
+                app:constraint_referenced_ids="pair_new_device_button,bluetooth_auto_on_toggle_info_text" />
+
             <Button
                 android:id="@+id/done_button"
                 style="@style/Widget.Dialog.Button"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:layout_marginTop="9dp"
                 android:layout_marginBottom="@dimen/dialog_bottom_padding"
                 android:layout_marginEnd="@dimen/dialog_side_padding"
                 android:layout_marginStart="@dimen/dialog_side_padding"
@@ -200,7 +271,9 @@
                 android:maxLines="1"
                 android:text="@string/inline_done_button"
                 app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintBottom_toBottomOf="parent" />
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/barrier"
+                app:layout_constraintVertical_bias="1" />
         </androidx.constraintlayout.widget.ConstraintLayout>
     </androidx.core.widget.NestedScrollView>
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index cc31754..7537a00 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1717,6 +1717,10 @@
     <dimen name="bluetooth_dialog_layout_margin">16dp</dimen>
     <!-- The height of the bluetooth device in bluetooth dialog. -->
     <dimen name="bluetooth_dialog_device_height">72dp</dimen>
+    <!-- The height of the main scroll view in bluetooth dialog. -->
+    <dimen name="bluetooth_dialog_scroll_view_min_height">145dp</dimen>
+    <!-- The height of the main scroll view in bluetooth dialog with auto on toggle. -->
+    <dimen name="bluetooth_dialog_scroll_view_min_height_with_auto_on">350dp</dimen>
 
     <!-- Height percentage of the parent container occupied by the communal view -->
     <item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 64c6cfa..e401c71 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -669,6 +669,10 @@
     <string name="accessibility_quick_settings_bluetooth_device_tap_to_disconnect">disconnect</string>
     <!-- QuickSettings: Accessibility label to activate a device [CHAR LIMIT=NONE]-->
     <string name="accessibility_quick_settings_bluetooth_device_tap_to_activate">activate</string>
+    <!-- QuickSettings: Bluetooth auto on tomorrow [CHAR LIMIT=NONE]-->
+    <string name="turn_on_bluetooth_auto_tomorrow">Automatically turn on again tomorrow</string>
+    <!-- QuickSettings: Bluetooth auto on info text [CHAR LIMIT=NONE]-->
+    <string name="turn_on_bluetooth_auto_info">Features like Quick Share, Find My Device, and device location use Bluetooth</string>
 
     <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
     <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 40d2d16..febfd4c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.media.dagger.MediaModule
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import javax.inject.Inject
 import javax.inject.Named
 import kotlinx.coroutines.CoroutineScope
@@ -51,6 +53,7 @@
     @Application private val scope: CoroutineScope,
     private val communalInteractor: CommunalInteractor,
     tutorialInteractor: CommunalTutorialInteractor,
+    shadeInteractor: ShadeInteractor,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
     @CommunalLog logBuffer: LogBuffer,
 ) : BaseCommunalViewModel(communalInteractor, mediaHost) {
@@ -81,6 +84,9 @@
     override val isPopupOnDismissCtaShowing: Flow<Boolean> =
         _isPopupOnDismissCtaShowing.asStateFlow()
 
+    /** Whether touches should be disabled in communal */
+    val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded)
+
     init {
         // Initialize our media host for the UMO. This only needs to happen once and must be done
         // before the MediaHierarchyManager attempts to move the UMO to the hub.
@@ -114,6 +120,7 @@
     }
 
     private var delayedHidePopupJob: Job? = null
+
     private fun schedulePopupHiding() {
         cancelDelayedPopupHiding()
         delayedHidePopupJob =
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 9a4dfdd..4e23ecd9 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
 import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
@@ -116,7 +117,7 @@
     ): View
 
     /** Creates a container that hosts the communal UI and handles gesture transitions. */
-    fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View
+    fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View
 
     /** Creates a [View] that represents the Lockscreen. */
     fun createLockscreen(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 9e7c70d..1b7a507 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.clocks.ClockController
@@ -112,6 +113,10 @@
         }
 
         val burnInParams = MutableStateFlow(BurnInParameters())
+        val viewState =
+            ViewStateAccessor(
+                alpha = { view.alpha },
+            )
 
         val disposableHandle =
             view.repeatWhenAttached {
@@ -134,7 +139,7 @@
 
                     if (keyguardBottomAreaRefactor()) {
                         launch {
-                            viewModel.alpha.collect { alpha ->
+                            viewModel.alpha(viewState).collect { alpha ->
                                 view.alpha = alpha
                                 childViews[statusViewId]?.alpha = alpha
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 1144efe..f95efaa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -43,6 +43,10 @@
 import android.window.InputTransferToken
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import androidx.core.view.isInvisible
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch
@@ -393,7 +397,7 @@
             ),
         )
 
-        setUpUdfps(previewContext, rootView)
+        setUpUdfps(previewContext, if (migrateClocksToBlueprint()) keyguardRootView else rootView)
 
         if (keyguardBottomAreaRefactor()) {
             setupShortcuts(keyguardRootView)
@@ -468,15 +472,6 @@
             return
         }
 
-        // Place the UDFPS view in the proper sensor location
-        val fingerprintLayoutParams =
-            FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height())
-        fingerprintLayoutParams.setMarginsRelative(
-            sensorBounds.left,
-            sensorBounds.top,
-            sensorBounds.right,
-            sensorBounds.bottom
-        )
         val finger =
             LayoutInflater.from(previewContext)
                 .inflate(
@@ -484,7 +479,31 @@
                     parentView,
                     false,
                 ) as View
-        parentView.addView(finger, fingerprintLayoutParams)
+
+        // Place the UDFPS view in the proper sensor location
+        if (migrateClocksToBlueprint()) {
+            finger.id = R.id.lock_icon_view
+            parentView.addView(finger)
+            val cs = ConstraintSet()
+            cs.clone(parentView as ConstraintLayout)
+            cs.apply {
+                constrainWidth(R.id.lock_icon_view, sensorBounds.width())
+                constrainHeight(R.id.lock_icon_view, sensorBounds.height())
+                connect(R.id.lock_icon_view, TOP, PARENT_ID, TOP, sensorBounds.top)
+                connect(R.id.lock_icon_view, START, PARENT_ID, START, sensorBounds.left)
+            }
+            cs.applyTo(parentView)
+        } else {
+            val fingerprintLayoutParams =
+                FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height())
+            fingerprintLayoutParams.setMarginsRelative(
+                sensorBounds.left,
+                sensorBounds.top,
+                sensorBounds.right,
+                sensorBounds.bottom
+            )
+            parentView.addView(finger, fingerprintLayoutParams)
+        }
     }
 
     private fun setUpClock(previewContext: Context, parentView: ViewGroup) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index d75a72f..75132a5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -24,11 +24,13 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -36,6 +38,7 @@
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 
@@ -52,6 +55,7 @@
     ambientState: AmbientState,
     controller: NotificationStackScrollLayoutController,
     notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
     @Main mainDispatcher: CoroutineDispatcher,
 ) :
     NotificationStackScrollLayoutSection(
@@ -74,12 +78,27 @@
             val bottomMargin =
                 context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
             if (migrateClocksToBlueprint()) {
+                val useLargeScreenHeader =
+                    context.resources.getBoolean(R.bool.config_use_large_screen_shade_header)
+                val marginTopLargeScreen =
+                    if (centralizedStatusBarDimensRefactor()) {
+                        largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
+                    } else {
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.large_screen_shade_header_height
+                        )
+                    }
                 connect(
                     R.id.nssl_placeholder,
                     TOP,
                     R.id.smart_space_barrier_bottom,
                     BOTTOM,
-                    bottomMargin
+                    bottomMargin +
+                        if (useLargeScreenHeader) {
+                            marginTopLargeScreen
+                        } else {
+                            0
+                        }
                 )
             } else {
                 connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index 756a4cc..3e35ae4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -23,13 +23,11 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -37,7 +35,6 @@
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
-import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 
@@ -56,7 +53,6 @@
     notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) :
     NotificationStackScrollLayoutSection(
         context,
@@ -75,16 +71,13 @@
             return
         }
         constraintSet.apply {
-            val splitShadeTopMargin =
-                if (centralizedStatusBarDimensRefactor()) {
-                    largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
-                } else {
-                    context.resources.getDimensionPixelSize(
-                        R.dimen.large_screen_shade_header_height
-                    )
-                }
-            connect(R.id.nssl_placeholder, TOP, PARENT_ID, TOP, splitShadeTopMargin)
-
+            connect(
+                R.id.nssl_placeholder,
+                TOP,
+                PARENT_ID,
+                TOP,
+                context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin)
+            )
             connect(R.id.nssl_placeholder, START, PARENT_ID, START)
             connect(R.id.nssl_placeholder, END, PARENT_ID, END)
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index ec13228..83be651 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -60,18 +60,21 @@
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val dozeParameters: DozeParameters,
     private val keyguardInteractor: KeyguardInteractor,
-    communalInteractor: CommunalInteractor,
+    private val communalInteractor: CommunalInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
-    aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
-    lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
-    alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel,
-    primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
-    lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
-    glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
-    screenOffAnimationController: ScreenOffAnimationController,
+    private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+    private val lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
+    private val alternateBouncerToGoneTransitionViewModel:
+        AlternateBouncerToGoneTransitionViewModel,
+    private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
+    private val lockscreenToGlanceableHubTransitionViewModel:
+        LockscreenToGlanceableHubTransitionViewModel,
+    private val glanceableHubToLockscreenTransitionViewModel:
+        GlanceableHubToLockscreenTransitionViewModel,
+    private val screenOffAnimationController: ScreenOffAnimationController,
     private val aodBurnInViewModel: AodBurnInViewModel,
-    aodAlphaViewModel: AodAlphaViewModel,
+    private val aodAlphaViewModel: AodAlphaViewModel,
 ) {
 
     val burnInLayerVisibility: Flow<Int> =
@@ -101,8 +104,8 @@
     val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds
 
     /** An observable for the alpha level for the entire keyguard root view. */
-    val alpha: Flow<Float> =
-        combine(
+    fun alpha(viewState: ViewStateAccessor): Flow<Float> {
+        return combine(
                 communalInteractor.isIdleOnCommunal,
                 // The transitions are mutually exclusive, so they are safe to merge to get the last
                 // value emitted by any of them. Do not add flows that cannot make this guarantee.
@@ -110,7 +113,7 @@
                     aodAlphaViewModel.alpha,
                     lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
                     glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
-                    lockscreenToGoneTransitionViewModel.lockscreenAlpha,
+                    lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState),
                     primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
                     alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
                 )
@@ -125,6 +128,7 @@
                 }
             }
             .distinctUntilChanged()
+    }
 
     /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */
     val lockscreenStateAlpha: Flow<Float> = aodToLockscreenTransitionViewModel.lockscreenAlpha
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 d981650..15459f4 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
@@ -16,10 +16,12 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow.FlowBuilder
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -37,7 +39,7 @@
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
 
-    private val transitionAnimation =
+    private val transitionAnimation: FlowBuilder =
         animationFlow.setup(
             duration = FromLockscreenTransitionInteractor.TO_GONE_DURATION,
             from = KeyguardState.LOCKSCREEN,
@@ -52,7 +54,26 @@
             onCancel = { 1f },
         )
 
-    val lockscreenAlpha: Flow<Float> = shortcutsAlpha
+    fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var startAlpha: Float? = null
+        return transitionAnimation.sharedFlow(
+            duration = 200.milliseconds,
+            onStep = {
+                if (startAlpha == null) {
+                    startAlpha = viewState.alpha()
+                }
+                MathUtils.lerp(startAlpha!!, 0f, it)
+            },
+            onFinish = {
+                startAlpha = null
+                0f
+            },
+            onCancel = {
+                startAlpha = null
+                1f
+            },
+        )
+    }
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt
new file mode 100644
index 0000000..cb5db86
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.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.keyguard.ui.viewmodel
+
+/** View-level state information to be shared between ui and viewmodel. */
+data class ViewStateAccessor(
+    val alpha: () -> Float = { 0f },
+    val translationY: () -> Int = { 0 },
+    val translationX: () -> Int = { 0 },
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index 40a9b9c..13d743f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -141,6 +141,7 @@
                 if (field != value) {
                     field = value
                     checkIfPollingNeeded()
+                    _data = _data.copy(listening = value)
                 }
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
new file mode 100644
index 0000000..dcae088
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.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.tiles.dialog.bluetooth
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */
+@SysUISingleton
+class BluetoothAutoOnInteractor
+@Inject
+constructor(
+    private val bluetoothAutoOnRepository: BluetoothAutoOnRepository,
+) {
+
+    val isEnabled = bluetoothAutoOnRepository.getValue.map { it == ENABLED }.distinctUntilChanged()
+
+    /**
+     * Checks if the auto on value is present in the repository.
+     *
+     * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server).
+     */
+    suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent()
+
+    /**
+     * Sets enabled or disabled based on the provided value.
+     *
+     * @param value `true` to enable the feature, `false` to disable it.
+     */
+    suspend fun setEnabled(value: Boolean) {
+        if (!isValuePresent()) {
+            Log.e(TAG, "Trying to set toggle value while feature not available.")
+        } else {
+            val newValue = if (value) ENABLED else DISABLED
+            bluetoothAutoOnRepository.setValue(newValue)
+        }
+    }
+
+    companion object {
+        private const val TAG = "BluetoothAutoOnInteractor"
+        const val DISABLED = 0
+        const val ENABLED = 1
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
new file mode 100644
index 0000000..e17b4d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.os.UserHandle
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.withContext
+
+/** Repository class responsible for managing the Bluetooth Auto-On feature settings. */
+// TODO(b/316822488): Handle multi-user
+@SysUISingleton
+class BluetoothAutoOnRepository
+@Inject
+constructor(
+    private val secureSettings: SecureSettings,
+    private val userRepository: UserRepository,
+    @Application private val coroutineScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+    // Flow representing the auto on setting value
+    internal val getValue: Flow<Int> =
+        secureSettings
+            .observerFlow(UserHandle.USER_SYSTEM, SETTING_NAME)
+            .onStart { emit(Unit) }
+            .map {
+                if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+                    Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+                    return@map UNSET
+                }
+                secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM)
+            }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
+
+    /**
+     * Checks if the auto on setting value is ever set for the current user.
+     *
+     * @return `true` if the setting value is not UNSET, `false` otherwise.
+     */
+    suspend fun isValuePresent(): Boolean =
+        withContext(backgroundDispatcher) {
+            if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+                Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+                false
+            } else {
+                secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM) != UNSET
+            }
+        }
+
+    /**
+     * Sets the Bluetooth Auto-On setting value for the current user.
+     *
+     * @param value The new setting value to be applied.
+     */
+    suspend fun setValue(value: Int) {
+        withContext(backgroundDispatcher) {
+            if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+                Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+            } else {
+                secureSettings.putIntForUser(SETTING_NAME, value, UserHandle.USER_SYSTEM)
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "BluetoothAutoOnRepository"
+        const val SETTING_NAME = "bluetooth_automatic_turn_on"
+        const val UNSET = -1
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
index 1a06c38..6b53c7a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
@@ -56,7 +56,7 @@
 internal class BluetoothTileDialog
 constructor(
     private val bluetoothToggleInitialValue: Boolean,
-    private val subtitleResIdInitialValue: Int,
+    private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
     private val cachedContentHeight: Int,
     private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
     @Main private val mainDispatcher: CoroutineDispatcher,
@@ -71,6 +71,10 @@
     internal val bluetoothStateToggle
         get() = mutableBluetoothStateToggle.asStateFlow()
 
+    private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+    internal val bluetoothAutoOnToggle
+        get() = mutableBluetoothAutoOnToggle.asStateFlow()
+
     private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> =
         MutableSharedFlow(extraBufferCapacity = 1)
     internal val deviceItemClick
@@ -89,6 +93,8 @@
 
     private lateinit var toggleView: Switch
     private lateinit var subtitleTextView: TextView
+    private lateinit var autoOnToggle: Switch
+    private lateinit var autoOnToggleView: View
     private lateinit var doneButton: View
     private lateinit var seeAllButton: View
     private lateinit var pairNewDeviceButton: View
@@ -108,6 +114,8 @@
 
         toggleView = requireViewById(R.id.bluetooth_toggle)
         subtitleTextView = requireViewById(R.id.bluetooth_tile_dialog_subtitle) as TextView
+        autoOnToggle = requireViewById(R.id.bluetooth_auto_on_toggle)
+        autoOnToggleView = requireViewById(R.id.bluetooth_auto_on_toggle_layout)
         doneButton = requireViewById(R.id.done_button)
         seeAllButton = requireViewById(R.id.see_all_button)
         pairNewDeviceButton = requireViewById(R.id.pair_new_device_button)
@@ -116,7 +124,7 @@
         setupToggle()
         setupRecyclerView()
 
-        subtitleTextView.text = context.getString(subtitleResIdInitialValue)
+        subtitleTextView.text = context.getString(initialUiProperties.subTitleResId)
         doneButton.setOnClickListener { dismiss() }
         seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) }
         pairNewDeviceButton.setOnClickListener {
@@ -124,7 +132,9 @@
         }
         requireViewById<View>(R.id.scroll_view).apply {
             scrollViewContent = this
-            layoutParams.height = cachedContentHeight
+            minimumHeight =
+                resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId)
+            layoutParams.height = maxOf(cachedContentHeight, minimumHeight)
         }
         progressBarAnimation = requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
         progressBarBackground = requireViewById(R.id.bluetooth_tile_dialog_progress_background)
@@ -178,13 +188,27 @@
         }
     }
 
-    internal fun onBluetoothStateUpdated(isEnabled: Boolean, subtitleResId: Int) {
+    internal fun onBluetoothStateUpdated(
+        isEnabled: Boolean,
+        uiProperties: BluetoothTileDialogViewModel.UiProperties
+    ) {
         toggleView.apply {
             isChecked = isEnabled
             setEnabled(true)
             alpha = ENABLED_ALPHA
         }
-        subtitleTextView.text = context.getString(subtitleResId)
+        subtitleTextView.text = context.getString(uiProperties.subTitleResId)
+        autoOnToggleView.visibility = uiProperties.autoOnToggleVisibility
+    }
+
+    internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean) {
+        if (::autoOnToggle.isInitialized) {
+            autoOnToggle.apply {
+                isChecked = isEnabled
+                setEnabled(true)
+                alpha = ENABLED_ALPHA
+            }
+        }
     }
 
     private fun setupToggle() {
@@ -198,6 +222,16 @@
             logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString())
             uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
         }
+
+        autoOnToggleView.visibility = initialUiProperties.autoOnToggleVisibility
+        autoOnToggle.setOnCheckedChangeListener { view, isChecked ->
+            mutableBluetoothAutoOnToggle.value = isChecked
+            view.apply {
+                isEnabled = false
+                alpha = DISABLED_ALPHA
+            }
+            uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
+        }
     }
 
     private fun setupRecyclerView() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
index 86e5dde..cd52e0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
@@ -31,7 +31,8 @@
     @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500),
     @UiEvent(doc = "Active device clicked to disconnect") ACTIVE_DEVICE_DISCONNECT(1507),
     @UiEvent(doc = "Connected other device clicked to disconnect")
-    CONNECTED_OTHER_DEVICE_DISCONNECT(1508);
+    CONNECTED_OTHER_DEVICE_DISCONNECT(1508),
+    @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617);
 
     override fun getId() = metricId
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
index 54bb95c..5a14e5f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -21,9 +21,15 @@
 import android.content.SharedPreferences
 import android.os.Bundle
 import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.view.ViewGroup
+import androidx.annotation.DimenRes
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.flags.Flags.bluetoothQsTileDialogAutoOnToggle
 import com.android.systemui.Prefs
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
@@ -58,6 +64,7 @@
 constructor(
     private val deviceItemInteractor: DeviceItemInteractor,
     private val bluetoothStateInteractor: BluetoothStateInteractor,
+    private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val activityStarter: ActivityStarter,
     private val systemClock: SystemClock,
@@ -143,7 +150,10 @@
                 bluetoothStateInteractor.bluetoothStateUpdate
                     .filterNotNull()
                     .onEach {
-                        dialog.onBluetoothStateUpdated(it, getSubtitleResId(it))
+                        dialog.onBluetoothStateUpdated(
+                            it,
+                            UiProperties.build(it, isAutoOnToggleFeatureAvailable())
+                        )
                         updateDeviceItemJob?.cancel()
                         updateDeviceItemJob = launch {
                             deviceItemInteractor.updateDeviceItems(
@@ -177,6 +187,21 @@
                     }
                     .launchIn(this)
 
+                if (isAutoOnToggleFeatureAvailable()) {
+                    // bluetoothAutoOnUpdate is emitted when bluetooth auto on on/off state is
+                    // changed.
+                    bluetoothAutoOnInteractor.isEnabled
+                        .onEach { dialog.onBluetoothAutoOnUpdated(it) }
+                        .launchIn(this)
+
+                    // bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on
+                    // switch, send the new value to the bluetoothAutoOnInteractor.
+                    dialog.bluetoothAutoOnToggle
+                        .filterNotNull()
+                        .onEach { bluetoothAutoOnInteractor.setEnabled(it) }
+                        .launchIn(this)
+                }
+
                 produce<Unit> { awaitClose { dialog.cancel() } }
             }
     }
@@ -192,7 +217,10 @@
 
         return BluetoothTileDialog(
                 bluetoothStateInteractor.isBluetoothEnabled,
-                getSubtitleResId(bluetoothStateInteractor.isBluetoothEnabled),
+                UiProperties.build(
+                    bluetoothStateInteractor.isBluetoothEnabled,
+                    isAutoOnToggleFeatureAvailable()
+                ),
                 cachedContentHeight,
                 this@BluetoothTileDialogViewModel,
                 mainDispatcher,
@@ -244,6 +272,10 @@
         }
     }
 
+    @VisibleForTesting
+    internal suspend fun isAutoOnToggleFeatureAvailable() =
+        bluetoothQsTileDialogAutoOnToggle() && bluetoothAutoOnInteractor.isValuePresent()
+
     companion object {
         private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog"
         private const val CONTENT_HEIGHT_PREF_KEY = Prefs.Key.BLUETOOTH_TILE_DIALOG_CONTENT_HEIGHT
@@ -251,6 +283,29 @@
             if (isBluetoothEnabled) R.string.quick_settings_bluetooth_tile_subtitle
             else R.string.bt_is_off
     }
+
+    internal data class UiProperties(
+        @StringRes val subTitleResId: Int,
+        val autoOnToggleVisibility: Int,
+        @DimenRes val scrollViewMinHeightResId: Int,
+    ) {
+        companion object {
+            internal fun build(
+                isBluetoothEnabled: Boolean,
+                isAutoOnToggleFeatureAvailable: Boolean
+            ) =
+                UiProperties(
+                    subTitleResId = getSubtitleResId(isBluetoothEnabled),
+                    autoOnToggleVisibility =
+                        if (isAutoOnToggleFeatureAvailable && !isBluetoothEnabled) VISIBLE
+                        else GONE,
+                    scrollViewMinHeightResId =
+                        if (isAutoOnToggleFeatureAvailable)
+                            R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on
+                        else R.dimen.bluetooth_dialog_scroll_view_min_height
+                )
+        }
+    }
 }
 
 internal interface BluetoothTileDialogCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
index 98c0217..2f0fc51 100644
--- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt
@@ -23,7 +23,6 @@
 import android.content.DialogInterface.BUTTON_POSITIVE
 import android.content.Intent
 import android.content.Intent.EXTRA_PACKAGE_NAME
-import android.content.pm.PackageManager
 import android.hardware.SensorPrivacyManager
 import android.hardware.SensorPrivacyManager.EXTRA_ALL_SENSORS
 import android.hardware.SensorPrivacyManager.EXTRA_SENSOR
@@ -32,7 +31,6 @@
 import android.os.Handler
 import android.window.OnBackInvokedDispatcher
 import androidx.annotation.OpenForTesting
-import com.android.internal.camera.flags.Flags
 import com.android.internal.util.FrameworkStatsLog.PRIVACY_TOGGLE_DIALOG_INTERACTION
 import com.android.internal.util.FrameworkStatsLog.PRIVACY_TOGGLE_DIALOG_INTERACTION__ACTION__CANCEL
 import com.android.internal.util.FrameworkStatsLog.PRIVACY_TOGGLE_DIALOG_INTERACTION__ACTION__ENABLE
@@ -92,14 +90,14 @@
             sensor = ALL_SENSORS
             val callback = IndividualSensorPrivacyController.Callback { _, _ ->
                 if (!sensorPrivacyController.isSensorBlocked(MICROPHONE) &&
-                        !isCameraBlocked(sensorUsePackageName)) {
+                        !sensorPrivacyController.isSensorBlocked(CAMERA)) {
                     finish()
                 }
             }
             sensorPrivacyListener = callback
             sensorPrivacyController.addCallback(callback)
             if (!sensorPrivacyController.isSensorBlocked(MICROPHONE) &&
-                    !isCameraBlocked(sensorUsePackageName)) {
+                    !sensorPrivacyController.isSensorBlocked(CAMERA)) {
                 finish()
                 return
             }
@@ -112,22 +110,14 @@
             }
             val callback = IndividualSensorPrivacyController.Callback {
                 whichSensor: Int, isBlocked: Boolean ->
-                if (whichSensor != sensor) {
-                    // Ignore a callback; we're not interested in.
-                } else if ((whichSensor == CAMERA) && !isCameraBlocked(sensorUsePackageName)) {
-                    finish()
-                } else if ((whichSensor == MICROPHONE) && !isBlocked) {
+                if (whichSensor == sensor && !isBlocked) {
                     finish()
                 }
             }
             sensorPrivacyListener = callback
             sensorPrivacyController.addCallback(callback)
 
-            if ((sensor == CAMERA) && !isCameraBlocked(sensorUsePackageName)) {
-                finish()
-                return
-            } else if ((sensor == MICROPHONE) &&
-                    !sensorPrivacyController.isSensorBlocked(MICROPHONE)) {
+            if (!sensorPrivacyController.isSensorBlocked(sensor)) {
                 finish()
                 return
             }
@@ -214,22 +204,6 @@
         recreate()
     }
 
-    private fun isAutomotive(): Boolean {
-        return getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
-    }
-
-    private fun isCameraBlocked(packageName: String): Boolean {
-        if (Flags.privacyAllowlist()) {
-            if (isAutomotive()) {
-                return sensorPrivacyController.isCameraPrivacyEnabled(packageName)
-            } else {
-                return sensorPrivacyController.isSensorBlocked(CAMERA)
-            }
-        } else {
-            return sensorPrivacyController.isSensorBlocked(CAMERA)
-        }
-    }
-
     private fun disableSensorPrivacy() {
         if (sensor == ALL_SENSORS) {
             sensorPrivacyController.setSensorBlocked(DIALOG, MICROPHONE, false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
index 90abec1..80c3551 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar;
 
+import static android.app.Flags.lifetimeExtensionRefactor;
+
 import android.annotation.NonNull;
 import android.app.Notification;
 import android.app.RemoteInputHistoryItem;
@@ -29,6 +31,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.stream.Stream;
 
@@ -68,7 +71,7 @@
     @NonNull
     public StatusBarNotification rebuildForCanceledSmartReplies(
             NotificationEntry entry) {
-        return rebuildWithRemoteInputInserted(entry, null /* remoteInputTest */,
+        return rebuildWithRemoteInputInserted(entry, null /* remoteInputText */,
                 false /* showSpinner */, null /* mimeType */, null /* uri */);
     }
 
@@ -97,22 +100,50 @@
     StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry,
             CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
         StatusBarNotification sbn = entry.getSbn();
-
         Notification.Builder b = Notification.Builder
                 .recoverBuilder(mContext, sbn.getNotification().clone());
-        if (remoteInputText != null || uri != null) {
-            RemoteInputHistoryItem newItem = uri != null
-                    ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
-                    : new RemoteInputHistoryItem(remoteInputText);
+
+        if (lifetimeExtensionRefactor()) {
+            if (entry.remoteInputs == null) {
+                entry.remoteInputs = new ArrayList<RemoteInputHistoryItem>();
+            }
+
+            // Append new remote input information to remoteInputs list
+            if (remoteInputText != null || uri != null) {
+                RemoteInputHistoryItem newItem = uri != null
+                        ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
+                        : new RemoteInputHistoryItem(remoteInputText);
+                // The list is latest-first, so new elements should be added as the first element.
+                entry.remoteInputs.add(0, newItem);
+            }
+
+            // Read the whole remoteInputs list from the entry, then append all of those to the sbn.
             Parcelable[] oldHistoryItems = sbn.getNotification().extras
                     .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+
             RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
                     ? Stream.concat(
-                    Stream.of(newItem),
-                    Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
+                            entry.remoteInputs.stream(),
+                            Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
                     .toArray(RemoteInputHistoryItem[]::new)
-                    : new RemoteInputHistoryItem[] { newItem };
+                    : entry.remoteInputs.toArray(RemoteInputHistoryItem[]::new);
             b.setRemoteInputHistory(newHistoryItems);
+
+        } else {
+            if (remoteInputText != null || uri != null) {
+                RemoteInputHistoryItem newItem = uri != null
+                        ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
+                        : new RemoteInputHistoryItem(remoteInputText);
+                Parcelable[] oldHistoryItems = sbn.getNotification().extras
+                        .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+                RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
+                        ? Stream.concat(
+                                Stream.of(newItem),
+                                Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
+                        .toArray(RemoteInputHistoryItem[]::new)
+                        : new RemoteInputHistoryItem[]{newItem};
+                b.setRemoteInputHistory(newHistoryItems);
+            }
         }
         b.setShowRemoteInputSpinner(showSpinner);
         b.setHideSmartReplies(true);
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 cdacb10..8678f0a 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
@@ -40,6 +40,7 @@
 import android.app.NotificationManager.Policy;
 import android.app.Person;
 import android.app.RemoteInput;
+import android.app.RemoteInputHistoryItem;
 import android.content.Context;
 import android.content.pm.ShortcutInfo;
 import android.net.Uri;
@@ -127,6 +128,7 @@
     public int targetSdk;
     private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
     public CharSequence remoteInputText;
+    public List<RemoteInputHistoryItem> remoteInputs = null;
     public String remoteInputMimeType;
     public Uri remoteInputUri;
     public ContentInfo remoteInputAttachment;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index 918bf08..28fff15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import android.app.Flags.lifetimeExtensionRefactor
+import android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
 import android.os.Handler
 import android.service.notification.NotificationListenerService.REASON_CANCEL
 import android.service.notification.NotificationListenerService.REASON_CLICK
@@ -88,11 +90,21 @@
 
     override fun attach(pipeline: NotifPipeline) {
         mNotificationRemoteInputManager.setRemoteInputListener(this)
-        mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
+        if (lifetimeExtensionRefactor()) {
+            pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender)
+        } else {
+            mRemoteInputLifetimeExtenders.forEach {
+                pipeline.addNotificationLifetimeExtender(it)
+            }
+        }
         mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
         pipeline.addCollectionListener(mCollectionListener)
     }
 
+    /*
+     * Listener that updates the appearance of the notification if it has been lifetime extended
+     * by a a direct reply or a smart reply, and cancelled.
+     */
     val mCollectionListener = object : NotifCollectionListener {
         override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
             if (DEBUG) {
@@ -100,9 +112,32 @@
                         " fromSystem=$fromSystem)")
             }
             if (fromSystem) {
-                // Mark smart replies as sent whenever a notification is updated by the app,
-                // otherwise the smart replies are never marked as sent.
-                mSmartReplyController.stopSending(entry)
+                if (lifetimeExtensionRefactor()) {
+                    if ((entry.getSbn().getNotification().flags
+                                    and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+                        if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
+                                        entry)) {
+                            val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+                            entry.onRemoteInputInserted()
+                            mNotifUpdater.onInternalNotificationUpdate(newSbn,
+                                    "Extending lifetime of notification with remote input")
+                        } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
+                                        entry)) {
+                            val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
+                            mSmartReplyController.stopSending(entry)
+                            mNotifUpdater.onInternalNotificationUpdate(newSbn,
+                                    "Extending lifetime of notification with smart reply")
+                        }
+                    } else {
+                        // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
+                        // should have their remote inputs list cleared.
+                        entry.remoteInputs = null
+                    }
+                } else {
+                    // Mark smart replies as sent whenever a notification is updated by the app,
+                    // otherwise the smart replies are never marked as sent.
+                    mSmartReplyController.stopSending(entry)
+                }
             }
         }
 
@@ -130,8 +165,10 @@
         // NOTE: This is some trickery! By removing the lifetime extensions when we know they should
         // be immediately re-upped, we ensure that the side-effects of the lifetime extenders get to
         // fire again, thus ensuring that we add subsequent replies to the notification.
-        mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
-        mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
+        if (!lifetimeExtensionRefactor()) {
+            mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
+            mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
+        }
 
         // If we're extending for remote input being active, then from the apps point of
         // view it is already canceled, so we'll need to cancel it on the apps behalf
@@ -160,15 +197,19 @@
     }
 
     override fun isNotificationKeptForRemoteInputHistory(key: String) =
+        if (!lifetimeExtensionRefactor()) {
             mRemoteInputHistoryExtender.isExtending(key) ||
                     mSmartReplyHistoryExtender.isExtending(key)
+        } else false
 
     override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
         if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
-        mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
-                REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
-        mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
-                REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+        if (!lifetimeExtensionRefactor()) {
+            mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+                    REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+            mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+                    REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+        }
         mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
                 REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index 3cdb2cd..d1aff80 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection.render;
 
+import android.util.Log;
+
 import androidx.annotation.NonNull;
 
 import com.android.systemui.Dumpable;
@@ -40,6 +42,8 @@
  */
 @SysUISingleton
 public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpable {
+    private static final String TAG = "GroupExpansionaManagerImpl";
+
     private final DumpManager mDumpManager;
     private final GroupMembershipManager mGroupMembershipManager;
     private final Set<OnGroupExpansionChangeListener> mOnGroupChangeListeners = new HashSet<>();
@@ -100,7 +104,7 @@
         NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
         if (entry.getParent() == null) {
             if (expanded) {
-                throw new IllegalArgumentException("Cannot expand group that is not attached");
+                Log.wtf(TAG, "Cannot expand group that is not attached");
             } else {
                 // The entry is no longer attached, but we still want to make sure we don't have
                 // a stale expansion state.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 47daf49..830b8c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1335,6 +1335,10 @@
         }
     }
 
+    public float getAlpha() {
+        return mView.getAlpha();
+    }
+
     public void setSuppressChildrenMeasureAndLayout(boolean suppressLayout) {
         mView.suppressChildrenMeasureAndLayout(suppressLayout);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
index b4f578f..ffab9ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
@@ -76,14 +76,10 @@
             }
         val nsslId = R.id.notification_stack_scroller
         constraintSet.apply {
-            connect(nsslId, START, startConstraintId, START)
-            connect(nsslId, END, PARENT_ID, END)
-            connect(nsslId, BOTTOM, PARENT_ID, BOTTOM)
-            connect(nsslId, TOP, PARENT_ID, TOP)
-            setMargin(nsslId, START, marginStart)
-            setMargin(nsslId, END, marginEnd)
-            setMargin(nsslId, TOP, marginTop)
-            setMargin(nsslId, BOTTOM, marginBottom)
+            connect(nsslId, START, startConstraintId, START, marginStart)
+            connect(nsslId, END, PARENT_ID, END, marginEnd)
+            connect(nsslId, BOTTOM, PARENT_ID, BOTTOM, marginBottom)
+            connect(nsslId, TOP, PARENT_ID, TOP, marginTop)
         }
         constraintSet.applyTo(this)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 97db9b6..daea8af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -24,6 +24,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -75,6 +76,10 @@
             }
 
         val burnInParams = MutableStateFlow(BurnInParameters())
+        val viewState =
+            ViewStateAccessor(
+                alpha = { controller.getAlpha() },
+            )
 
         /*
          * For animation sensitive coroutines, immediately run just like applicationScope does
@@ -141,7 +146,7 @@
 
                     if (!sceneContainerFlags.isEnabled()) {
                         launch {
-                            viewModel.expansionAlpha.collect {
+                            viewModel.expansionAlpha(viewState).collect {
                                 controller.setMaxAlphaForExpansion(it)
                             }
                         }
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 811da51..ff00cb3 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
@@ -50,6 +50,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
@@ -67,7 +68,6 @@
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
@@ -100,21 +100,35 @@
         setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER)
 
     private val edgeToAlphaViewModel =
-        mapOf<Edge?, Flow<Float>>(
+        mapOf<Edge?, (ViewStateAccessor) -> Flow<Float>>(
             Edge(from = LOCKSCREEN, to = DREAMING) to
-                lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
+                { _: ViewStateAccessor ->
+                    lockscreenToDreamingTransitionViewModel.lockscreenAlpha
+                },
             Edge(from = LOCKSCREEN, to = GONE) to
-                lockscreenToGoneTransitionViewModel.lockscreenAlpha,
+                { viewState: ViewStateAccessor ->
+                    lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState)
+                },
             Edge(from = ALTERNATE_BOUNCER, to = GONE) to
-                alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
+                { _: ViewStateAccessor ->
+                    alternateBouncerToGoneTransitionViewModel.lockscreenAlpha
+                },
             Edge(from = PRIMARY_BOUNCER, to = GONE) to
-                primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
+                { _: ViewStateAccessor ->
+                    primaryBouncerToGoneTransitionViewModel.lockscreenAlpha
+                },
             Edge(from = DREAMING, to = LOCKSCREEN) to
-                dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
+                { _: ViewStateAccessor ->
+                    dreamingToLockscreenTransitionViewModel.lockscreenAlpha
+                },
             Edge(from = LOCKSCREEN, to = OCCLUDED) to
-                lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
+                { _: ViewStateAccessor ->
+                    lockscreenToOccludedTransitionViewModel.lockscreenAlpha
+                },
             Edge(from = OCCLUDED, to = LOCKSCREEN) to
-                occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+                { _: ViewStateAccessor ->
+                    occludedToLockscreenTransitionViewModel.lockscreenAlpha
+                },
         )
 
     private val lockscreenTransitionInProgress: Flow<Edge?> =
@@ -151,21 +165,20 @@
     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
         interactor.configurationBasedDimensions
             .map {
+                val marginTop =
+                    if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop
                 ConfigurationBasedDimensions(
                     marginStart = if (it.useSplitShade) 0 else it.marginHorizontal,
                     marginEnd = it.marginHorizontal,
                     marginBottom = it.marginBottom,
-                    marginTop =
-                        if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop,
+                    marginTop = marginTop,
                     useSplitShade = it.useSplitShade,
                     paddingTop =
                         if (it.useSplitShade) {
-                            // When in split shade, the margin is applied twice as the legacy shade
-                            // code uses it to calculate padding.
-                            it.keyguardSplitShadeTopMargin - 2 * it.marginTopLargeScreen
+                            marginTop
                         } else {
                             0
-                        }
+                        },
                 )
             }
             .distinctUntilChanged()
@@ -255,13 +268,15 @@
                 isOnLockscreenWithoutShade,
                 keyguardInteractor.notificationContainerBounds,
                 configurationBasedDimensions,
-                interactor.topPosition.sampleCombine(
-                    keyguardTransitionInteractor.isInTransitionToAnyState,
-                    shadeInteractor.qsExpansion,
-                ),
+                interactor.topPosition
+                    .sampleCombine(
+                        keyguardTransitionInteractor.isInTransitionToAnyState,
+                        shadeInteractor.qsExpansion,
+                    )
+                    .onStart { emit(Triple(0f, false, 0f)) }
             ) { onLockscreen, bounds, config, (top, isInTransitionToAnyState, qsExpansion) ->
                 if (onLockscreen) {
-                    bounds.copy(top = bounds.top + config.paddingTop)
+                    bounds.copy(top = bounds.top - config.paddingTop)
                 } else {
                     // When QS expansion > 0, it should directly set the top padding so do not
                     // animate it
@@ -278,46 +293,63 @@
                 initialValue = NotificationContainerBounds(),
             )
 
-    /** As QS is expanding, fade out notifications unless in splitshade */
-    private val alphaForQsExpansion: Flow<Float> =
-        interactor.configurationBasedDimensions.flatMapLatest {
-            if (it.useSplitShade) {
-                flowOf(1f)
-            } else {
-                shadeInteractor.qsExpansion.map { 1f - it }
+    /**
+     * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
+     * notifications unless in splitshade.
+     */
+    private val alphaForShadeAndQsExpansion: Flow<Float> =
+        interactor.configurationBasedDimensions
+            .flatMapLatest { configurationBasedDimensions ->
+                combine(
+                    shadeInteractor.shadeExpansion,
+                    shadeInteractor.qsExpansion,
+                ) { shadeExpansion, qsExpansion ->
+                    if (shadeExpansion > 0f || qsExpansion > 0f) {
+                        if (configurationBasedDimensions.useSplitShade) {
+                            1f
+                        } else {
+                            // Fade as QS shade expands
+                            1f - qsExpansion
+                        }
+                    } else {
+                        // Not visible unless the shade/qs is visible
+                        0f
+                    }
+                }
             }
-        }
+            .distinctUntilChanged()
 
-    val expansionAlpha: Flow<Float> =
+    fun expansionAlpha(viewState: ViewStateAccessor): Flow<Float> {
         // Due to issues with the legacy shade, some shade expansion events are sent incorrectly,
         // such as when the shade resets. This can happen while the transition to/from LOCKSCREEN
         // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while
         // those transitions are in progress. Without this, the alpha value will produce a visible
         // flicker.
-        lockscreenTransitionInProgress
+        return lockscreenTransitionInProgress
             .flatMapLatest { edge ->
-                edgeToAlphaViewModel.getOrElse(
+                edgeToAlphaViewModel.getOrDefault(
                     edge,
-                    {
+                    { _: ViewStateAccessor ->
                         isOnLockscreenWithoutShade.flatMapLatest { isOnLockscreenWithoutShade ->
                             combineTransform(
                                 keyguardInteractor.keyguardAlpha,
                                 shadeCollpaseFadeIn,
-                                alphaForQsExpansion,
-                            ) { alpha, shadeCollpaseFadeIn, alphaForQsExpansion ->
+                                alphaForShadeAndQsExpansion,
+                            ) { alpha, shadeCollpaseFadeIn, alphaForShadeAndQsExpansion ->
                                 if (isOnLockscreenWithoutShade) {
                                     if (!shadeCollpaseFadeIn) {
                                         emit(alpha)
                                     }
                                 } else {
-                                    emit(alphaForQsExpansion)
+                                    emit(alphaForShadeAndQsExpansion)
                                 }
                             }
                         }
                     }
-                )
+                )(viewState)
             }
             .distinctUntilChanged()
+    }
 
     /**
      * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index ca3e3c6..db55da7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -43,6 +43,7 @@
  */
 public class KeyguardClockPositionAlgorithm {
     private static final String TAG = "KeyguardClockPositionAlgorithm";
+    private static final boolean DEBUG = false;
 
     /**
      * Margin between the bottom of the status view and the notification shade.
@@ -318,24 +319,26 @@
         }
 
         float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY);
-        float clockYDark = clockY
-                + fullyDarkBurnInOffset
-                + shift;
+        float clockYDark = clockY + fullyDarkBurnInOffset + shift;
         mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount);
-        final String inputs = "panelExpansion: " + panelExpansion + " darkAmount: " + darkAmount;
-        final String outputs = "clockY: " + clockY
-                + " burnInPreventionOffsetY: " + burnInPreventionOffsetY
-                + " fullyDarkBurnInOffset: " + fullyDarkBurnInOffset
-                + " shift: " + shift
-                + " mOverStretchAmount: " + mOverStretchAmount
-                + " mCurrentBurnInOffsetY: " + mCurrentBurnInOffsetY;
-        mLogger.i(msg -> {
-            return msg.getStr1() + " -> " + msg.getStr2();
-        }, msg -> {
-            msg.setStr1(inputs);
-            msg.setStr2(outputs);
-            return kotlin.Unit.INSTANCE;
-        });
+
+        if (DEBUG) {
+            final float finalShift = shift;
+            final float finalBurnInPreventionOffsetY = burnInPreventionOffsetY;
+            mLogger.i(msg -> {
+                final String inputs = "panelExpansion: " + panelExpansion
+                        + " darkAmount: " + darkAmount;
+                final String outputs = "clockY: " + clockY
+                        + " burnInPreventionOffsetY: " + finalBurnInPreventionOffsetY
+                        + " fullyDarkBurnInOffset: " + fullyDarkBurnInOffset
+                        + " shift: " + finalShift
+                        + " mOverStretchAmount: " + mOverStretchAmount
+                        + " mCurrentBurnInOffsetY: " + mCurrentBurnInOffsetY;
+                return inputs + " -> " + outputs;
+            }, msg -> {
+                return kotlin.Unit.INSTANCE;
+            });
+        }
         return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 45bdae8..8ac3b4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -132,9 +132,9 @@
         if (updateDisplayParameters()) {
             updateLayoutForCutout();
             requestLayout();
-            if (truncatedStatusBarIconsFix()) {
-                updateWindowHeight();
-            }
+        }
+        if (truncatedStatusBarIconsFix()) {
+            updateWindowHeight();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
index fb67358..eb08f37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java
@@ -16,12 +16,9 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.annotation.FlaggedApi;
 import android.hardware.SensorPrivacyManager.Sensors.Sensor;
 import android.hardware.SensorPrivacyManager.Sources.Source;
 
-import com.android.internal.camera.flags.Flags;
-
 public interface IndividualSensorPrivacyController extends
         CallbackController<IndividualSensorPrivacyController.Callback> {
     void init();
@@ -45,12 +42,6 @@
      */
     boolean requiresAuthentication();
 
-    /**
-     * @return whether camera privacy is enabled for the package.
-     */
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    boolean isCameraPrivacyEnabled(String packageName);
-
     interface Callback {
         void onSensorBlockedChanged(@Sensor int sensor, boolean blocked);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
index 8f768e9..87dfc99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
@@ -19,9 +19,6 @@
 import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
 import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
 
-import android.Manifest;
-import android.annotation.FlaggedApi;
-import android.annotation.RequiresPermission;
 import android.hardware.SensorPrivacyManager;
 import android.hardware.SensorPrivacyManager.Sensors.Sensor;
 import android.hardware.SensorPrivacyManager.Sources.Source;
@@ -31,8 +28,6 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.internal.camera.flags.Flags;
-
 import java.util.Set;
 
 public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPrivacyController {
@@ -107,13 +102,6 @@
     }
 
     @Override
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-    public boolean isCameraPrivacyEnabled(String packageName) {
-        return mSensorPrivacyManager.isCameraPrivacyEnabled(packageName);
-    }
-
-    @Override
     public void addCallback(@NonNull Callback listener) {
         mCallbacks.add(listener);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
index 8b05a54..e7aaddd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
@@ -19,6 +19,7 @@
 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.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -56,6 +57,41 @@
             deviceEntryParentViewAlpha.forEach { assertThat(it).isEqualTo(0f) }
         }
 
+    @Test
+    fun lockscreenAlphaStartsFromViewStateAccessorAlpha() =
+        testScope.runTest {
+            val viewState = ViewStateAccessor(alpha = { 0.5f })
+            val alpha by collectLastValue(underTest.lockscreenAlpha(viewState))
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+            repository.sendTransitionStep(step(0f))
+            assertThat(alpha).isEqualTo(0.5f)
+
+            repository.sendTransitionStep(step(0.25f))
+            assertThat(alpha).isEqualTo(0.25f)
+
+            repository.sendTransitionStep(step(.5f))
+            assertThat(alpha).isEqualTo(0f)
+        }
+
+    @Test
+    fun lockscreenAlphaWithNoViewStateAccessorValue() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.lockscreenAlpha(ViewStateAccessor()))
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+            repository.sendTransitionStep(step(0f))
+            assertThat(alpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(0.25f))
+            assertThat(alpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(0.5f))
+            assertThat(alpha).isEqualTo(0f)
+        }
+
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
index 100e579..4ec29ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
@@ -164,6 +164,18 @@
     }
 
     @Test
+    fun seekbarNotListeningNotScrubbingPlaying() {
+        // WHEN playing
+        val isPlaying = true
+        val isScrubbing = false
+        val data =
+            SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000, false)
+        observer.onChanged(data)
+        // THEN progress drawable is not animating
+        verify(mockSquigglyProgress).animate = false
+    }
+
+    @Test
     fun seekBarPlayingScrubbing() {
         // WHEN playing & scrubbing
         val isPlaying = true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
new file mode 100644
index 0000000..3710713
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.dialog.bluetooth
+
+import android.content.pm.UserInfo
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BluetoothAutoOnInteractorTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+    private var secureSettings: FakeSettings = FakeSettings()
+    private val userRepository: FakeUserRepository = FakeUserRepository()
+    private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor
+
+    @Before
+    fun setUp() {
+        bluetoothAutoOnInteractor =
+            BluetoothAutoOnInteractor(
+                BluetoothAutoOnRepository(
+                    secureSettings,
+                    userRepository,
+                    testScope.backgroundScope,
+                    testDispatcher
+                )
+            )
+    }
+
+    @Test
+    fun testSet_bluetoothAutoOnUnset_doNothing() {
+        testScope.runTest {
+            bluetoothAutoOnInteractor.setEnabled(true)
+
+            val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
+
+            runCurrent()
+
+            Truth.assertThat(actualValue).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun testSet_bluetoothAutoOnSet_setNewValue() {
+        testScope.runTest {
+            userRepository.setUserInfos(listOf(SYSTEM_USER))
+            secureSettings.putIntForUser(
+                BluetoothAutoOnRepository.SETTING_NAME,
+                BluetoothAutoOnInteractor.DISABLED,
+                SYSTEM_USER_ID
+            )
+            bluetoothAutoOnInteractor.setEnabled(true)
+
+            val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
+
+            runCurrent()
+
+            Truth.assertThat(actualValue).isEqualTo(true)
+        }
+    }
+
+    companion object {
+        private const val SYSTEM_USER_ID = 0
+        private val SYSTEM_USER =
+            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
new file mode 100644
index 0000000..8986d99
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.dialog.bluetooth
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.DISABLED
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.ENABLED
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.SETTING_NAME
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.UNSET
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BluetoothAutoOnRepositoryTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+    private var secureSettings: FakeSettings = FakeSettings()
+    private val userRepository: FakeUserRepository = FakeUserRepository()
+
+    private lateinit var bluetoothAutoOnRepository: BluetoothAutoOnRepository
+
+    @Before
+    fun setUp() {
+        bluetoothAutoOnRepository =
+            BluetoothAutoOnRepository(
+                secureSettings,
+                userRepository,
+                testScope.backgroundScope,
+                testDispatcher
+            )
+
+        userRepository.setUserInfos(listOf(SECONDARY_USER, SYSTEM_USER))
+    }
+
+    @Test
+    fun testGetValue_valueUnset() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(SYSTEM_USER)
+            val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+            runCurrent()
+
+            assertThat(actualValue).isEqualTo(UNSET)
+            assertThat(bluetoothAutoOnRepository.isValuePresent()).isFalse()
+        }
+    }
+
+    @Test
+    fun testGetValue_valueFalse() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(SYSTEM_USER)
+            val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+            secureSettings.putIntForUser(SETTING_NAME, DISABLED, UserHandle.USER_SYSTEM)
+            runCurrent()
+
+            assertThat(actualValue).isEqualTo(DISABLED)
+        }
+    }
+
+    @Test
+    fun testGetValue_valueTrue() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(SYSTEM_USER)
+            val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+            secureSettings.putIntForUser(SETTING_NAME, ENABLED, UserHandle.USER_SYSTEM)
+            runCurrent()
+
+            assertThat(actualValue).isEqualTo(ENABLED)
+        }
+    }
+
+    @Test
+    fun testGetValue_valueTrue_secondaryUser_returnUnset() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(SECONDARY_USER)
+            val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+            secureSettings.putIntForUser(SETTING_NAME, ENABLED, SECONDARY_USER_ID)
+            runCurrent()
+
+            assertThat(actualValue).isEqualTo(UNSET)
+        }
+    }
+
+    companion object {
+        private const val SYSTEM_USER_ID = 0
+        private const val SECONDARY_USER_ID = 1
+        private val SYSTEM_USER =
+            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+        private val SECONDARY_USER =
+            UserInfo(/* id= */ SECONDARY_USER_ID, /* name= */ "secondary user", /* flags= */ 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
index 154aa1c..70b0417 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
@@ -71,7 +71,11 @@
 
     @Mock private lateinit var logger: BluetoothTileDialogLogger
 
-    private val subtitleResId = R.string.quick_settings_bluetooth_tile_subtitle
+    private val uiProperties =
+        BluetoothTileDialogViewModel.UiProperties.build(
+            isBluetoothEnabled = ENABLED,
+            isAutoOnToggleFeatureAvailable = ENABLED
+        )
 
     private val fakeSystemClock = FakeSystemClock()
 
@@ -90,7 +94,7 @@
         bluetoothTileDialog =
             BluetoothTileDialog(
                 ENABLED,
-                subtitleResId,
+                uiProperties,
                 CONTENT_HEIGHT,
                 bluetoothTileDialogCallback,
                 dispatcher,
@@ -131,7 +135,7 @@
             bluetoothTileDialog =
                 BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     CONTENT_HEIGHT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -166,7 +170,7 @@
         val viewHolder =
             BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     CONTENT_HEIGHT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -194,7 +198,7 @@
         val viewHolder =
             BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     CONTENT_HEIGHT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -219,7 +223,7 @@
             bluetoothTileDialog =
                 BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     CONTENT_HEIGHT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -253,12 +257,36 @@
     }
 
     @Test
-    fun testShowDialog_displayFromCachedHeight() {
+    fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() {
+        testScope.runTest {
+            val cachedHeight = Int.MAX_VALUE
+            bluetoothTileDialog =
+                BluetoothTileDialog(
+                    ENABLED,
+                    uiProperties,
+                    cachedHeight,
+                    bluetoothTileDialogCallback,
+                    dispatcher,
+                    fakeSystemClock,
+                    uiEventLogger,
+                    logger,
+                    mContext
+                )
+            bluetoothTileDialog.show()
+            assertThat(
+                    bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height
+                )
+                .isEqualTo(cachedHeight)
+        }
+    }
+
+    @Test
+    fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() {
         testScope.runTest {
             bluetoothTileDialog =
                 BluetoothTileDialog(
                     ENABLED,
-                    subtitleResId,
+                    uiProperties,
                     MATCH_PARENT,
                     bluetoothTileDialogCallback,
                     dispatcher,
@@ -271,7 +299,32 @@
             assertThat(
                     bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height
                 )
-                .isEqualTo(MATCH_PARENT)
+                .isGreaterThan(MATCH_PARENT)
+        }
+    }
+
+    @Test
+    fun testShowDialog_bluetoothEnabled_autoOnToggleGone() {
+        testScope.runTest {
+            bluetoothTileDialog =
+                BluetoothTileDialog(
+                    ENABLED,
+                    BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+                    MATCH_PARENT,
+                    bluetoothTileDialogCallback,
+                    dispatcher,
+                    fakeSystemClock,
+                    uiEventLogger,
+                    logger,
+                    mContext
+                )
+            bluetoothTileDialog.show()
+            assertThat(
+                    bluetoothTileDialog
+                        .requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout)
+                        .visibility
+                )
+                .isEqualTo(GONE)
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
index 98ac17b..cb9f4b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -17,20 +17,27 @@
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
 import android.content.SharedPreferences
+import android.content.pm.UserInfo
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.widget.LinearLayout
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.flags.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -84,16 +91,36 @@
     private lateinit var scheduler: TestCoroutineScheduler
     private lateinit var dispatcher: CoroutineDispatcher
     private lateinit var testScope: TestScope
+    private lateinit var secureSettings: FakeSettings
+    private lateinit var userRepository: FakeUserRepository
 
     @Before
     fun setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
         scheduler = TestCoroutineScheduler()
         dispatcher = UnconfinedTestDispatcher(scheduler)
         testScope = TestScope(dispatcher)
+        secureSettings = FakeSettings()
+        userRepository = FakeUserRepository()
+        userRepository.setUserInfos(listOf(SYSTEM_USER))
+        secureSettings.putIntForUser(
+            BluetoothAutoOnRepository.SETTING_NAME,
+            BluetoothAutoOnInteractor.ENABLED,
+            SYSTEM_USER_ID
+        )
         bluetoothTileDialogViewModel =
             BluetoothTileDialogViewModel(
                 deviceItemInteractor,
                 bluetoothStateInteractor,
+                // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor.
+                BluetoothAutoOnInteractor(
+                    BluetoothAutoOnRepository(
+                        secureSettings,
+                        userRepository,
+                        testScope.backgroundScope,
+                        dispatcher
+                    )
+                ),
                 mDialogTransitionAnimator,
                 activityStarter,
                 fakeSystemClock,
@@ -174,4 +201,64 @@
             verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
         }
     }
+
+    @Test
+    fun testBuildUiProperties_bluetoothOn_shouldHideAutoOn() {
+        testScope.runTest {
+            val actual =
+                BluetoothTileDialogViewModel.UiProperties.build(
+                    isBluetoothEnabled = true,
+                    isAutoOnToggleFeatureAvailable = true
+                )
+            assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
+        }
+    }
+
+    @Test
+    fun testBuildUiProperties_bluetoothOff_shouldShowAutoOn() {
+        testScope.runTest {
+            val actual =
+                BluetoothTileDialogViewModel.UiProperties.build(
+                    isBluetoothEnabled = false,
+                    isAutoOnToggleFeatureAvailable = true
+                )
+            assertThat(actual.autoOnToggleVisibility).isEqualTo(VISIBLE)
+        }
+    }
+
+    @Test
+    fun testBuildUiProperties_bluetoothOff_autoOnFeatureUnavailable_shouldHideAutoOn() {
+        testScope.runTest {
+            val actual =
+                BluetoothTileDialogViewModel.UiProperties.build(
+                    isBluetoothEnabled = false,
+                    isAutoOnToggleFeatureAvailable = false
+                )
+            assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
+        }
+    }
+
+    @Test
+    fun testIsAutoOnToggleFeatureAvailable_flagOn_settingValueSet_returnTrue() {
+        testScope.runTest {
+            val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
+            assertThat(actual).isTrue()
+        }
+    }
+
+    @Test
+    fun testIsAutoOnToggleFeatureAvailable_flagOff_settingValueSet_returnFalse() {
+        testScope.runTest {
+            mSetFlagsRule.disableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
+
+            val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
+            assertThat(actual).isFalse()
+        }
+    }
+
+    companion object {
+        private const val SYSTEM_USER_ID = 0
+        private val SYSTEM_USER =
+            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
index 7073cc7..85b8b03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
@@ -15,7 +15,13 @@
  */
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import android.app.Flags.lifetimeExtensionRefactor
+import android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR
+import android.app.Notification
+import android.app.RemoteInputHistoryItem
 import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.service.notification.StatusBarNotification
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
@@ -34,6 +40,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.captureMany
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -42,6 +49,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations.initMocks
 
@@ -57,6 +65,7 @@
     private lateinit var entry2: NotificationEntry
 
     @Mock private lateinit var lifetimeExtensionCallback: OnEndLifetimeExtensionCallback
+
     @Mock private lateinit var rebuilder: RemoteInputNotificationRebuilder
     @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
     @Mock private lateinit var mainHandler: Handler
@@ -84,9 +93,6 @@
         listener = withArgCaptor {
             verify(remoteInputManager).setRemoteInputListener(capture())
         }
-        collectionListener = withArgCaptor {
-            verify(pipeline).addCollectionListener(capture())
-        }
         entry1 = NotificationEntryBuilder().setId(1).build()
         entry2 = NotificationEntryBuilder().setId(2).build()
         `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
@@ -98,16 +104,23 @@
     val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender
     val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender
 
+    val collectionListeners get() = captureMany {
+        verify(pipeline, times(1)).addCollectionListener(capture())
+    }
+
     @Test
     fun testRemoteInputActive() {
         `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
         assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue()
-        assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
-        assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+        if (!lifetimeExtensionRefactor()) {
+            assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+            assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+        }
         assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isFalse()
     }
 
     @Test
+    @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testRemoteInputHistory() {
         `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry1)).thenReturn(true)
         assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse()
@@ -117,6 +130,7 @@
     }
 
     @Test
+    @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testSmartReplyHistory() {
         `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry1)).thenReturn(true)
         assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse()
@@ -142,4 +156,81 @@
         verify(lifetimeExtensionCallback).onEndLifetimeExtension(remoteInputActiveExtender, entry1)
         assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
     }
+
+    @Test
+    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+    fun testOnlyRemoteInputActiveLifetimeExtenderExtends() {
+        `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
+        assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue()
+        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isTrue()
+
+        listener.onPanelCollapsed()
+        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+
+        // Checks that lifetimeExtensionCallback is only called the expected number of times,
+        // by the remoteInputActiveExtender.
+        // Checks that the remote input history extender and smart reply history extenders
+        // aren't attached to the pipeline.
+        verify(lifetimeExtensionCallback, times(1)).onEndLifetimeExtension(any(), any())
+    }
+
+    @Test
+    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+    fun testRemoteInputLifetimeExtensionListenerTrigger() {
+        // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+        val entry = NotificationEntryBuilder()
+                .setId(3)
+                .setTag("entry")
+                .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
+                .build()
+        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(true)
+        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
+
+        collectionListeners.forEach {
+            it.onEntryUpdated(entry, true)
+        }
+
+        verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry)
+    }
+
+    @Test
+    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+    fun testSmartReplyLifetimeExtensionListenerTrigger() {
+        // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+        val entry = NotificationEntryBuilder()
+                .setId(3)
+                .setTag("entry")
+                .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
+                .build()
+        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
+        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(true)
+        collectionListeners.forEach {
+            it.onEntryUpdated(entry, true)
+        }
+
+
+        verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry)
+        verify(smartReplyController, times(1)).stopSending(entry)
+    }
+
+    @Test
+    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+    fun testLifetimeExtensionListenerClearsRemoteInputs() {
+        // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+        val entry = NotificationEntryBuilder()
+                .setId(3)
+                .setTag("entry")
+                .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false)
+                .build()
+        entry.remoteInputs = ArrayList<RemoteInputHistoryItem>()
+        entry.remoteInputs.add(RemoteInputHistoryItem("Test Text"))
+        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
+        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
+
+        collectionListeners.forEach {
+            it.onEntryUpdated(entry, true)
+        }
+
+        assertThat(entry.remoteInputs).isNull()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index ff882b1..9055ba4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -138,7 +138,8 @@
 
             configurationRepository.onAnyConfigurationChange()
 
-            assertThat(dimens!!.paddingTop).isEqualTo(30)
+            // Should directly use the header height (flagged off value)
+            assertThat(dimens!!.paddingTop).isEqualTo(10)
         }
 
     @Test
@@ -154,7 +155,8 @@
 
             configurationRepository.onAnyConfigurationChange()
 
-            assertThat(dimens!!.paddingTop).isEqualTo(40)
+            // Should directly use the header height (flagged on value)
+            assertThat(dimens!!.paddingTop).isEqualTo(5)
         }
 
     @Test
@@ -456,8 +458,8 @@
             )
             runCurrent()
 
-            // Top should be equal to bounds (1) + padding adjustment (30)
-            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 31f, bottom = 2f))
+            // Top should be equal to bounds (1) - padding adjustment (10)
+            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = -9f, bottom = 2f))
         }
 
     @Test
@@ -483,8 +485,8 @@
             )
             runCurrent()
 
-            // Top should be equal to bounds (1) + padding adjustment (40)
-            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 41f, bottom = 2f))
+            // Top should be equal to bounds (1) - padding adjustment (5)
+            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = -4f, bottom = 2f))
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index bbf9a6b..38698f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -45,6 +45,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -80,6 +81,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mStaticMockSession = mockitoSession()
+                .strictness(Strictness.WARN)
                 .mockStatic(BurnInHelperKt.class)
                 .mockStatic(LargeScreenHeaderHelper.class)
                 .startMocking();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
index 6eb1c1a..4293a27 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -42,6 +42,7 @@
 import org.junit.Test
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 
 @SmallTest
@@ -145,6 +146,18 @@
     }
 
     @Test
+    fun onConfigurationChanged_multipleCalls_flagEnabled_updatesWindowHeightMultipleTimes() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX)
+
+        view.onConfigurationChanged(Configuration())
+        view.onConfigurationChanged(Configuration())
+        view.onConfigurationChanged(Configuration())
+        view.onConfigurationChanged(Configuration())
+
+        verify(windowController, times(4)).refreshStatusBarHeight()
+    }
+
+    @Test
     fun onConfigurationChanged_flagDisabled_doesNotUpdateWindowHeight() {
         mSetFlagsRule.disableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX)
 
@@ -154,6 +167,18 @@
     }
 
     @Test
+    fun onConfigurationChanged_multipleCalls_flagDisabled_doesNotUpdateWindowHeight() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX)
+
+        view.onConfigurationChanged(Configuration())
+        view.onConfigurationChanged(Configuration())
+        view.onConfigurationChanged(Configuration())
+        view.onConfigurationChanged(Configuration())
+
+        verify(windowController, never()).refreshStatusBarHeight()
+    }
+
+    @Test
     fun onAttachedToWindow_updatesLeftTopRightPaddingsBasedOnInsets() {
         val insets = Insets.of(/* left = */ 10, /* top = */ 20, /* right = */ 30, /* bottom = */ 40)
         whenever(contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt
new file mode 100644
index 0000000..7c36a85
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt
@@ -0,0 +1,267 @@
+/*
+ * 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.surfaceeffects.loadingeffect
+
+import android.graphics.Paint
+import android.graphics.RenderEffect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.model.SysUiStateTest
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_IN
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_OUT
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.MAIN
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.NOT_PLAYING
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationStateChangedCallback
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.PaintDrawCallback
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.RenderEffectDrawCallback
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class LoadingEffectTest : SysUiStateTest() {
+
+    private val fakeSystemClock = FakeSystemClock()
+    private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+    @Test
+    fun play_paintCallback_triggersDrawCallback() {
+        var paintFromCallback: Paint? = null
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {
+                    paintFromCallback = loadingPaint
+                }
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                TurbulenceNoiseAnimationConfig(),
+                paintCallback = drawCallback,
+                animationStateChangedCallback = null
+            )
+
+        fakeExecutor.execute {
+            assertThat(paintFromCallback).isNull()
+
+            loadingEffect.play()
+            fakeSystemClock.advanceTime(500L)
+
+            assertThat(paintFromCallback).isNotNull()
+        }
+    }
+
+    @Test
+    fun play_renderEffectCallback_triggersDrawCallback() {
+        var renderEffectFromCallback: RenderEffect? = null
+        val drawCallback =
+            object : RenderEffectDrawCallback {
+                override fun onDraw(loadingRenderEffect: RenderEffect) {
+                    renderEffectFromCallback = loadingRenderEffect
+                }
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                TurbulenceNoiseAnimationConfig(),
+                renderEffectCallback = drawCallback,
+                animationStateChangedCallback = null
+            )
+
+        fakeExecutor.execute {
+            assertThat(renderEffectFromCallback).isNull()
+
+            loadingEffect.play()
+            fakeSystemClock.advanceTime(500L)
+
+            assertThat(renderEffectFromCallback).isNotNull()
+        }
+    }
+
+    @Test
+    fun play_animationStateChangesInOrder() {
+        val config = TurbulenceNoiseAnimationConfig()
+        val expectedStates = arrayOf(NOT_PLAYING, EASE_IN, MAIN, EASE_OUT, NOT_PLAYING)
+        val actualStates = mutableListOf(NOT_PLAYING)
+        val stateChangedCallback =
+            object : AnimationStateChangedCallback {
+                override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+                    actualStates.add(newState)
+                }
+            }
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                stateChangedCallback
+            )
+
+        val timeToAdvance =
+            config.easeInDuration + config.maxDuration + config.easeOutDuration + 100
+
+        fakeExecutor.execute {
+            loadingEffect.play()
+
+            fakeSystemClock.advanceTime(timeToAdvance.toLong())
+
+            assertThat(actualStates).isEqualTo(expectedStates)
+        }
+    }
+
+    @Test
+    fun play_alreadyPlaying_playsOnlyOnce() {
+        val config = TurbulenceNoiseAnimationConfig()
+        var numPlay = 0
+        val stateChangedCallback =
+            object : AnimationStateChangedCallback {
+                override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+                    if (oldState == NOT_PLAYING && newState == EASE_IN) {
+                        numPlay++
+                    }
+                }
+            }
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                stateChangedCallback
+            )
+
+        fakeExecutor.execute {
+            assertThat(numPlay).isEqualTo(0)
+
+            loadingEffect.play()
+            loadingEffect.play()
+            loadingEffect.play()
+            loadingEffect.play()
+            loadingEffect.play()
+
+            assertThat(numPlay).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun finish_finishesLoadingEffect() {
+        val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        var isFinished = false
+        val stateChangedCallback =
+            object : AnimationStateChangedCallback {
+                override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+                    if (oldState == MAIN && newState == NOT_PLAYING) {
+                        isFinished = true
+                    }
+                }
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                stateChangedCallback
+            )
+
+        fakeExecutor.execute {
+            assertThat(isFinished).isFalse()
+
+            loadingEffect.play()
+            fakeSystemClock.advanceTime(config.easeInDuration.toLong() + 500L)
+
+            assertThat(isFinished).isFalse()
+
+            loadingEffect.finish()
+
+            assertThat(isFinished).isTrue()
+        }
+    }
+
+    @Test
+    fun finish_notMainState_hasNoEffect() {
+        val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        var isFinished = false
+        val stateChangedCallback =
+            object : AnimationStateChangedCallback {
+                override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+                    if (oldState == MAIN && newState == NOT_PLAYING) {
+                        isFinished = true
+                    }
+                }
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                stateChangedCallback
+            )
+
+        fakeExecutor.execute {
+            assertThat(isFinished).isFalse()
+
+            loadingEffect.finish()
+
+            assertThat(isFinished).isFalse()
+        }
+    }
+
+    @Test
+    fun getNoiseOffset_returnsNoiseOffset() {
+        val expectedNoiseOffset = arrayOf(0.1f, 0.2f, 0.3f)
+        val config =
+            TurbulenceNoiseAnimationConfig(
+                noiseOffsetX = expectedNoiseOffset[0],
+                noiseOffsetY = expectedNoiseOffset[1],
+                noiseOffsetZ = expectedNoiseOffset[2]
+            )
+        val drawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(loadingPaint: Paint) {}
+            }
+        val loadingEffect =
+            LoadingEffect(
+                baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                config,
+                paintCallback = drawCallback,
+                animationStateChangedCallback = null
+            )
+
+        assertThat(loadingEffect.getNoiseOffset()).isEqualTo(expectedNoiseOffset)
+    }
+}
diff --git a/ravenwood/mockito/Android.bp b/ravenwood/mockito/Android.bp
index a74bca4..95c7394 100644
--- a/ravenwood/mockito/Android.bp
+++ b/ravenwood/mockito/Android.bp
@@ -13,6 +13,9 @@
     srcs: [
         "test/**/*.java",
     ],
+    exclude_srcs: [
+        "test/**/*DeviceOnly*.java",
+    ],
     static_libs: [
         "junit",
         "truth",
@@ -31,6 +34,9 @@
     srcs: [
         "test/**/*.java",
     ],
+    exclude_srcs: [
+        "test/**/*RavenwoodOnly*.java",
+    ],
     static_libs: [
         "junit",
         "truth",
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java
new file mode 100644
index 0000000..d02fe69
--- /dev/null
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.ravenwood.mockito;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.ActivityManager;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.quality.Strictness;
+
+public class RavenwoodMockitoDeviceOnlyTest {
+    @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Test
+    public void testStaticMockOnDevice() {
+        var mockingSession = ExtendedMockito.mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .mockStatic(ActivityManager.class)
+                .startMocking();
+        try {
+            ExtendedMockito.doReturn(true).when(ActivityManager::isUserAMonkey);
+
+            assertThat(ActivityManager.isUserAMonkey()).isEqualTo(true);
+        } finally {
+            mockingSession.finishMocking();
+        }
+    }
+}
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java
new file mode 100644
index 0000000..0c137d5
--- /dev/null
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java
@@ -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.ravenwood.mockito;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.ActivityManager;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+public class RavenwoodMockitoRavenwoodOnlyTest {
+    @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Test
+    public void testStaticMockOnRavenwood() {
+        try (MockedStatic<ActivityManager> am = Mockito.mockStatic(ActivityManager.class)) {
+            am.when(ActivityManager::isUserAMonkey).thenReturn(true);
+            assertThat(ActivityManager.isUserAMonkey()).isEqualTo(true);
+        }
+    }
+}
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
index 1284d64..9566710 100644
--- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
@@ -31,28 +31,6 @@
 public class RavenwoodMockitoTest {
     @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule();
 
-
-// Use this to mock static methods, which isn't supported by mockito 2.
-// Mockito supports static mocking since 3.4.0:
-// See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48
-
-//    private MockitoSession mMockingSession;
-//
-//    @Before
-//    public void setUp() {
-//        mMockingSession = mockitoSession()
-//                .strictness(Strictness.LENIENT)
-//                .mockStatic(RavenwoodMockitoTest.class)
-//                .startMocking();
-//    }
-//
-//    @After
-//    public void tearDown() {
-//        if (mMockingSession != null) {
-//            mMockingSession.finishMocking();
-//        }
-//    }
-
     @Test
     public void testMockJdkClass() {
         Process object = mock(Process.class);
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 01c0074..927ddd7 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -13,11 +13,13 @@
 com.android.internal.os.Clock
 com.android.internal.os.LongArrayMultiStateCounter
 com.android.internal.os.LongArrayMultiStateCounter$LongArrayContainer
+com.android.internal.os.LongMultiStateCounter
 com.android.internal.os.MonotonicClock
 com.android.internal.os.PowerProfile
 com.android.internal.os.PowerStats
 com.android.internal.os.PowerStats$Descriptor
 com.android.internal.os.RuntimeInit
+com.android.internal.power.EnergyConsumerStats
 com.android.internal.power.ModemPowerProfile
 
 android.util.AtomicFile
@@ -54,6 +56,7 @@
 android.os.BatteryUsageStatsQuery
 android.os.Binder
 android.os.Binder$IdentitySupplier
+android.os.BluetoothBatteryStats
 android.os.Broadcaster
 android.os.Build
 android.os.BundleMerger
@@ -83,12 +86,15 @@
 android.os.ThreadLocalWorkSource
 android.os.TimestampedValue
 android.os.Trace
+android.os.UserBatteryConsumer
+android.os.UserBatteryConsumer$Builder
 android.os.UidBatteryConsumer
 android.os.UidBatteryConsumer$Builder
 android.os.UserHandle
 android.os.UserManager
 android.os.VibrationAttributes
 android.os.VibrationAttributes$Builder
+android.os.WakeLockStats
 android.os.WorkSource
 
 android.content.ClipData
@@ -159,6 +165,7 @@
 
 android.metrics.LogMaker
 
+android.view.Display
 android.view.Display$HdrCapabilities
 android.view.Display$Mode
 android.view.DisplayInfo
@@ -169,7 +176,6 @@
 android.telephony.ServiceState
 
 com.android.server.LocalServices
-com.android.server.power.stats.BatteryStatsImpl
 
 com.android.internal.util.BitUtils
 com.android.internal.util.BitwiseInputStream
@@ -192,6 +198,7 @@
 com.android.internal.util.RingBuffer
 com.android.internal.util.StringPool
 
+com.android.internal.os.BackgroundThread
 com.android.internal.os.BinderCallHeavyHitterWatcher
 com.android.internal.os.BinderDeathDispatcher
 com.android.internal.os.BinderfsStatsReader
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index b64c74e..af47ed2 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -42,10 +42,12 @@
 import android.accessibilityservice.AccessibilityTrace;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.MagnificationConfig;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -58,6 +60,7 @@
 import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.usb.UsbDevice;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -2776,4 +2779,23 @@
         t.close();
         mOverlays.clear();
     }
+
+    @Override
+    @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class
+    public void connectBluetoothBrailleDisplay(String bluetoothAddress,
+            IBrailleDisplayController controller) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void connectUsbBrailleDisplay(UsbDevice usbDevice,
+            IBrailleDisplayController controller) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 5ebe161..b90a66a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -26,16 +26,25 @@
 import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityTrace;
+import android.accessibilityservice.BrailleDisplayController;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.accessibilityservice.TouchInteractionController;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ParceledListSlice;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -44,6 +53,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Slog;
 import android.view.Display;
 import android.view.MotionEvent;
@@ -55,6 +65,8 @@
 import com.android.server.wm.WindowManagerInternal;
 
 import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -82,6 +94,9 @@
     final Intent mIntent;
     final ActivityTaskManagerInternal mActivityTaskManagerService;
 
+    private BrailleDisplayConnection mBrailleDisplayConnection;
+    private List<Bundle> mTestBrailleDisplays = null;
+
     private final Handler mMainHandler;
 
     private static final class AccessibilityInputMethodSessionCallback
@@ -448,6 +463,16 @@
         }
     }
 
+    @Override
+    public void resetLocked() {
+        super.resetLocked();
+        if (android.view.accessibility.Flags.brailleDisplayHid()) {
+            if (mBrailleDisplayConnection != null) {
+                mBrailleDisplayConnection.disconnect();
+            }
+        }
+    }
+
     public boolean isAccessibilityButtonAvailableLocked(AccessibilityUserState userState) {
         // If the service does not request the accessibility button, it isn't available
         if (!mRequestAccessibilityButton) {
@@ -640,4 +665,123 @@
             }
         }
     }
+
+    private void checkAccessibilityAccessLocked() {
+        if (!hasRightsToCurrentUserLocked()
+                || !mSecurityPolicy.checkAccessibilityAccess(this)) {
+            throw new SecurityException("Caller does not have accessibility access");
+        }
+    }
+
+    /**
+     * Sets up a BrailleDisplayConnection interface for the requested Bluetooth-connected
+     * Braille display.
+     *
+     * @param bluetoothAddress The address from
+     *                         {@link android.bluetooth.BluetoothDevice#getAddress()}.
+     */
+    @Override
+    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+    public void connectBluetoothBrailleDisplay(
+            @NonNull String bluetoothAddress, @NonNull IBrailleDisplayController controller) {
+        if (!android.view.accessibility.Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+        Objects.requireNonNull(bluetoothAddress);
+        Objects.requireNonNull(controller);
+        mContext.enforceCallingPermission(Manifest.permission.BLUETOOTH_CONNECT,
+                "Missing BLUETOOTH_CONNECT permission");
+        if (!BluetoothAdapter.checkBluetoothAddress(bluetoothAddress)) {
+            throw new IllegalArgumentException(
+                    bluetoothAddress + " is not a valid Bluetooth address");
+        }
+        synchronized (mLock) {
+            checkAccessibilityAccessLocked();
+            if (mBrailleDisplayConnection != null) {
+                throw new IllegalStateException(
+                        "This service already has a connected Braille display");
+            }
+            BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this);
+            if (mTestBrailleDisplays != null) {
+                connection.setTestData(mTestBrailleDisplays);
+            }
+            connection.connectLocked(
+                    bluetoothAddress, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
+        }
+    }
+
+    /**
+     * Sets up a BrailleDisplayConnection interface for the requested USB-connected
+     * Braille display.
+     *
+     * <p>The caller package must already have USB permission for this {@link UsbDevice}.
+     */
+    @SuppressLint("MissingPermission") // system_server has the required MANAGE_USB permission
+    @Override
+    @NonNull
+    public void connectUsbBrailleDisplay(@NonNull UsbDevice usbDevice,
+            @NonNull IBrailleDisplayController controller) {
+        if (!android.view.accessibility.Flags.brailleDisplayHid()) {
+            throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+        }
+        Objects.requireNonNull(usbDevice);
+        Objects.requireNonNull(controller);
+        final UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+        final String usbSerialNumber;
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            if (usbManager == null || !usbManager.hasPermission(
+                    usbDevice, mComponentName.getPackageName(), /*pid=*/ pid, /*uid=*/ uid)) {
+                throw new SecurityException(
+                        "Caller does not have permission to access this UsbDevice");
+            }
+            usbSerialNumber = usbDevice.getSerialNumber();
+            if (TextUtils.isEmpty(usbSerialNumber)) {
+                // If the UsbDevice does not report a serial number for locating the HIDRAW
+                // node then notify connection error ERROR_BRAILLE_DISPLAY_NOT_FOUND.
+                try {
+                    controller.onConnectionFailed(BrailleDisplayController.BrailleDisplayCallback
+                            .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+                } catch (RemoteException e) {
+                    Slog.e(LOG_TAG, "Error calling onConnectionFailed", e);
+                }
+                return;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+        synchronized (mLock) {
+            checkAccessibilityAccessLocked();
+            if (mBrailleDisplayConnection != null) {
+                throw new IllegalStateException(
+                        "This service already has a connected Braille display");
+            }
+            BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this);
+            if (mTestBrailleDisplays != null) {
+                connection.setTestData(mTestBrailleDisplays);
+            }
+            connection.connectLocked(
+                    usbSerialNumber, BrailleDisplayConnection.BUS_USB, controller);
+        }
+    }
+
+    @Override
+    @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+    public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {
+        // Enforce that this TestApi is only called by trusted (test) callers.
+        mContext.enforceCallingPermission(Manifest.permission.MANAGE_ACCESSIBILITY,
+                "Missing MANAGE_ACCESSIBILITY permission");
+        mTestBrailleDisplays = brailleDisplays;
+    }
+
+    void onBrailleDisplayConnectedLocked(BrailleDisplayConnection connection) {
+        mBrailleDisplayConnection = connection;
+    }
+
+    // Reset state when the BrailleDisplayConnection object disconnects itself.
+    void onBrailleDisplayDisconnectedLocked() {
+        mBrailleDisplayConnection = null;
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
new file mode 100644
index 0000000..1f18e15
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
@@ -0,0 +1,534 @@
+/*
+ * 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.accessibility;
+
+import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND;
+import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS;
+
+import android.accessibilityservice.BrailleDisplayController;
+import android.accessibilityservice.IBrailleDisplayConnection;
+import android.accessibilityservice.IBrailleDisplayController;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.PermissionManuallyEnforced;
+import android.annotation.RequiresNoPermission;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * This class represents the connection between {@code system_server} and a connected
+ * Braille Display using the Braille Display HID standard (usage page 0x41).
+ */
+class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
+    private static final String LOG_TAG = "BrailleDisplayConnection";
+
+    /**
+     * Represents the connection type of a Braille display.
+     *
+     * <p>The integer values must match the kernel's bus type values because this bus type is
+     * used to locate the correct HIDRAW node using data from the kernel. These values come
+     * from the UAPI header file bionic/libc/kernel/uapi/linux/input.h, which is guaranteed
+     * to stay constant.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, prefix = {"BUS_"}, value = {
+            BUS_UNKNOWN,
+            BUS_USB,
+            BUS_BLUETOOTH
+    })
+    @interface BusType {
+    }
+    static final int BUS_UNKNOWN = -1;
+    static final int BUS_USB = 0x03;
+    static final int BUS_BLUETOOTH = 0x05;
+
+    // Access to this static object must be guarded by a lock that is shared for all instances
+    // of this class: the singular Accessibility system_server lock (mLock).
+    private static final Set<File> sConnectedNodes = new ArraySet<>();
+
+    // Used to guard to AIDL methods from concurrent calls.
+    // Lock must match the one used by AccessibilityServiceConnection, which itself
+    // comes from AccessibilityManagerService.
+    private final Object mLock;
+    private final AccessibilityServiceConnection mServiceConnection;
+
+
+    private File mHidrawNode;
+    private IBrailleDisplayController mController;
+
+    private Thread mInputThread;
+    private OutputStream mOutputStream;
+    private HandlerThread mOutputThread;
+
+    // mScanner is not final because tests may modify this to use a test-only scanner.
+    private BrailleDisplayScanner mScanner;
+
+    BrailleDisplayConnection(@NonNull Object lock,
+            @NonNull AccessibilityServiceConnection serviceConnection) {
+        this.mLock = Objects.requireNonNull(lock);
+        this.mScanner = getDefaultNativeScanner(getDefaultNativeInterface());
+        this.mServiceConnection = Objects.requireNonNull(serviceConnection);
+    }
+
+    /**
+     * Interface to scan for properties of connected Braille displays.
+     *
+     * <p>Helps simplify testing Braille Display APIs using test data without requiring
+     * a real Braille display to be connected to the device, by using a test implementation
+     * of this interface.
+     *
+     * @see #getDefaultNativeScanner
+     * @see #setTestData
+     */
+    @VisibleForTesting
+    interface BrailleDisplayScanner {
+        Collection<Path> getHidrawNodePaths();
+
+        byte[] getDeviceReportDescriptor(@NonNull Path path);
+
+        String getUniqueId(@NonNull Path path);
+
+        @BusType
+        int getDeviceBusType(@NonNull Path path);
+    }
+
+    /**
+     * Finds the Braille display HIDRAW node associated with the provided unique ID.
+     *
+     * <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}.
+     */
+    void connectLocked(
+            @NonNull String expectedUniqueId,
+            @BusType int expectedBusType,
+            @NonNull IBrailleDisplayController controller) {
+        Objects.requireNonNull(expectedUniqueId);
+        this.mController = Objects.requireNonNull(controller);
+
+        final List<Pair<File, byte[]>> result = new ArrayList<>();
+        final Collection<Path> hidrawNodePaths = mScanner.getHidrawNodePaths();
+        if (hidrawNodePaths == null) {
+            Slog.w(LOG_TAG, "Unable to access the HIDRAW node directory");
+            sendConnectionErrorLocked(FLAG_ERROR_CANNOT_ACCESS);
+            return;
+        }
+        boolean unableToGetDescriptor = false;
+        // For every present HIDRAW device node:
+        for (Path path : hidrawNodePaths) {
+            final byte[] descriptor = mScanner.getDeviceReportDescriptor(path);
+            if (descriptor == null) {
+                unableToGetDescriptor = true;
+                continue;
+            }
+            final String uniqueId = mScanner.getUniqueId(path);
+            if (isBrailleDisplay(descriptor)
+                    && mScanner.getDeviceBusType(path) == expectedBusType
+                    && expectedUniqueId.equalsIgnoreCase(uniqueId)) {
+                result.add(Pair.create(path.toFile(), descriptor));
+            }
+        }
+
+        // Return success only when exactly one matching device node is found.
+        if (result.size() != 1) {
+            @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode =
+                    FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND;
+            // If we were unable to get some /dev/hidraw* descriptor then tell the accessibility
+            // service that the device may not have proper access to these device nodes.
+            if (unableToGetDescriptor) {
+                Slog.w(LOG_TAG, "Unable to access some HIDRAW node's descriptor");
+                errorCode |= FLAG_ERROR_CANNOT_ACCESS;
+            } else {
+                Slog.w(LOG_TAG,
+                        "Unable to find a unique Braille display matching the provided device");
+            }
+            sendConnectionErrorLocked(errorCode);
+            return;
+        }
+
+        this.mHidrawNode = result.get(0).first;
+        final byte[] reportDescriptor = result.get(0).second;
+
+        // Only one connection instance should exist for this hidraw node, across
+        // all currently running accessibility services.
+        if (sConnectedNodes.contains(this.mHidrawNode)) {
+            Slog.w(LOG_TAG,
+                    "Unable to find an unused Braille display matching the provided device");
+            sendConnectionErrorLocked(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+            return;
+        }
+        sConnectedNodes.add(this.mHidrawNode);
+
+        startReadingLocked();
+
+        try {
+            mServiceConnection.onBrailleDisplayConnectedLocked(this);
+            mController.onConnected(this, reportDescriptor);
+        } catch (RemoteException e) {
+            Slog.e(LOG_TAG, "Error calling onConnected", e);
+            disconnect();
+        }
+    }
+
+    private void sendConnectionErrorLocked(
+            @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode) {
+        try {
+            mController.onConnectionFailed(errorCode);
+        } catch (RemoteException e) {
+            Slog.e(LOG_TAG, "Error calling onConnectionFailed", e);
+        }
+    }
+
+    /** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */
+    private static boolean isBrailleDisplay(byte[] descriptor) {
+        // TODO: b/316036493 - Check that descriptor includes 0x41 reports.
+        return true;
+    }
+
+    /**
+     * Checks that the AccessibilityService that owns this BrailleDisplayConnection
+     * is still connected to the system.
+     *
+     * @throws IllegalStateException if not connected
+     */
+    private void assertServiceIsConnectedLocked() {
+        if (!mServiceConnection.isConnectedLocked()) {
+            throw new IllegalStateException("Accessibility service is not connected");
+        }
+    }
+
+    /**
+     * Disconnects from this Braille display. This object is no longer valid after
+     * this call returns.
+     */
+    @Override
+    // This is a cleanup method, so allow the call even if the calling service was disabled.
+    @RequiresNoPermission
+    public void disconnect() {
+        synchronized (mLock) {
+            closeInputLocked();
+            closeOutputLocked();
+            mServiceConnection.onBrailleDisplayDisconnectedLocked();
+            try {
+                mController.onDisconnected();
+            } catch (RemoteException e) {
+                Slog.e(LOG_TAG, "Error calling onDisconnected");
+            }
+            sConnectedNodes.remove(this.mHidrawNode);
+        }
+    }
+
+    /**
+     * Writes the provided HID bytes to this Braille display.
+     *
+     * <p>Writes are posted to a background thread handler.
+     *
+     * @param buffer The bytes to write to the Braille display. These bytes should be formatted
+     *               according to the report descriptor.
+     */
+    @Override
+    @PermissionManuallyEnforced // by assertServiceIsConnectedLocked()
+    public void write(@NonNull byte[] buffer) {
+        Objects.requireNonNull(buffer);
+        if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
+            Slog.e(LOG_TAG, "Requested write of size " + buffer.length
+                    + " which is larger than maximum " + IBinder.getSuggestedMaxIpcSizeBytes());
+            return;
+        }
+        synchronized (mLock) {
+            assertServiceIsConnectedLocked();
+            if (mOutputThread == null) {
+                try {
+                    mOutputStream = new FileOutputStream(mHidrawNode);
+                } catch (FileNotFoundException e) {
+                    Slog.e(LOG_TAG, "Unable to create write stream", e);
+                    disconnect();
+                    return;
+                }
+                mOutputThread = new HandlerThread("BrailleDisplayConnection output thread",
+                        Process.THREAD_PRIORITY_BACKGROUND);
+                mOutputThread.setDaemon(true);
+                mOutputThread.start();
+            }
+            // TODO: b/316035785 - Proactively disconnect a misbehaving Braille display by calling
+            //  disconnect() if the mOutputThread handler queue grows too large.
+            mOutputThread.getThreadHandler().post(() -> {
+                try {
+                    mOutputStream.write(buffer);
+                } catch (IOException e) {
+                    Slog.d(LOG_TAG, "Error writing to connected Braille display", e);
+                    disconnect();
+                }
+            });
+        }
+    }
+
+    /**
+     * Starts reading HID bytes from this Braille display.
+     *
+     * <p>Reads are performed on a background thread.
+     */
+    private void startReadingLocked() {
+        mInputThread = new Thread(() -> {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            try (InputStream inputStream = new FileInputStream(mHidrawNode)) {
+                final byte[] buffer = new byte[IBinder.getSuggestedMaxIpcSizeBytes()];
+                int readSize;
+                while (!Thread.interrupted()) {
+                    if (!mHidrawNode.exists()) {
+                        disconnect();
+                        break;
+                    }
+                    // Reading from the HIDRAW character device node will block
+                    // until bytes are available.
+                    readSize = inputStream.read(buffer);
+                    if (readSize > 0) {
+                        try {
+                            // Send the input to the AccessibilityService.
+                            mController.onInput(Arrays.copyOfRange(buffer, 0, readSize));
+                        } catch (RemoteException e) {
+                            // Error communicating with the AccessibilityService.
+                            Slog.e(LOG_TAG, "Error calling onInput", e);
+                            disconnect();
+                            break;
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                Slog.d(LOG_TAG, "Error reading from connected Braille display", e);
+                disconnect();
+            }
+        }, "BrailleDisplayConnection input thread");
+        mInputThread.setDaemon(true);
+        mInputThread.start();
+    }
+
+    /** Stop the Input thread. */
+    private void closeInputLocked() {
+        if (mInputThread != null) {
+            mInputThread.interrupt();
+        }
+        mInputThread = null;
+    }
+
+    /** Stop the Output thread and close the Output stream. */
+    private void closeOutputLocked() {
+        if (mOutputThread != null) {
+            mOutputThread.quit();
+        }
+        mOutputThread = null;
+        if (mOutputStream != null) {
+            try {
+                mOutputStream.close();
+            } catch (IOException e) {
+                Slog.e(LOG_TAG, "Unable to close output stream", e);
+            }
+        }
+        mOutputStream = null;
+    }
+
+    /**
+     * Returns a {@link BrailleDisplayScanner} that opens {@link FileInputStream}s to read
+     * from HIDRAW nodes and perform ioctls using the provided {@link NativeInterface}.
+     */
+    @VisibleForTesting
+    BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) {
+        Objects.requireNonNull(nativeInterface);
+        return new BrailleDisplayScanner() {
+            private static final Path DEVICE_DIR = Path.of("/dev");
+            private static final String HIDRAW_DEVICE_GLOB = "hidraw*";
+
+            @Override
+            public Collection<Path> getHidrawNodePaths() {
+                final List<Path> result = new ArrayList<>();
+                try (DirectoryStream<Path> hidrawNodePaths = Files.newDirectoryStream(
+                        DEVICE_DIR, HIDRAW_DEVICE_GLOB)) {
+                    for (Path path : hidrawNodePaths) {
+                        result.add(path);
+                    }
+                    return result;
+                } catch (IOException e) {
+                    return null;
+                }
+            }
+
+            private <T> T readFromFileDescriptor(Path path, Function<Integer, T> readFn) {
+                try (FileInputStream stream = new FileInputStream(path.toFile())) {
+                    return readFn.apply(stream.getFD().getInt$());
+                } catch (IOException e) {
+                    return null;
+                }
+            }
+
+            @Override
+            public byte[] getDeviceReportDescriptor(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                return readFromFileDescriptor(path, fd -> {
+                    final int descSize = nativeInterface.getHidrawDescSize(fd);
+                    if (descSize > 0) {
+                        return nativeInterface.getHidrawDesc(fd, descSize);
+                    }
+                    return null;
+                });
+            }
+
+            @Override
+            public String getUniqueId(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                return readFromFileDescriptor(path, nativeInterface::getHidrawUniq);
+            }
+
+            @Override
+            public int getDeviceBusType(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                Integer busType = readFromFileDescriptor(path, nativeInterface::getHidrawBusType);
+                return busType != null ? busType : BUS_UNKNOWN;
+            }
+        };
+    }
+
+    /**
+     * Sets test data to be used by CTS tests.
+     *
+     * <p>Replaces the default {@link BrailleDisplayScanner} object for this connection,
+     * and also returns it to allow unit testing this test-only implementation.
+     *
+     * @see BrailleDisplayController#setTestBrailleDisplayData
+     */
+    BrailleDisplayScanner setTestData(@NonNull List<Bundle> brailleDisplays) {
+        Objects.requireNonNull(brailleDisplays);
+        final Map<Path, Bundle> brailleDisplayMap = new ArrayMap<>();
+        for (Bundle brailleDisplay : brailleDisplays) {
+            Path hidrawNodePath = Path.of(brailleDisplay.getString(
+                    BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH));
+            brailleDisplayMap.put(hidrawNodePath, brailleDisplay);
+        }
+        synchronized (mLock) {
+            mScanner = new BrailleDisplayScanner() {
+                @Override
+                public Collection<Path> getHidrawNodePaths() {
+                    return brailleDisplayMap.keySet();
+                }
+
+                @Override
+                public byte[] getDeviceReportDescriptor(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getByteArray(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR);
+                }
+
+                @Override
+                public String getUniqueId(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getString(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID);
+                }
+
+                @Override
+                public int getDeviceBusType(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getBoolean(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH)
+                            ? BUS_BLUETOOTH : BUS_USB;
+                }
+            };
+            return mScanner;
+        }
+    }
+
+    /**
+     * This interface exists to support unit testing {@link #getDefaultNativeScanner}.
+     */
+    @VisibleForTesting
+    interface NativeInterface {
+        int getHidrawDescSize(int fd);
+
+        byte[] getHidrawDesc(int fd, int descSize);
+
+        String getHidrawUniq(int fd);
+
+        int getHidrawBusType(int fd);
+    }
+
+    /** Native interface that actually calls native HIDRAW ioctls. */
+    private NativeInterface getDefaultNativeInterface() {
+        return new NativeInterface() {
+            @Override
+            public int getHidrawDescSize(int fd) {
+                return nativeGetHidrawDescSize(fd);
+            }
+
+            @Override
+            public byte[] getHidrawDesc(int fd, int descSize) {
+                return nativeGetHidrawDesc(fd, descSize);
+            }
+
+            @Override
+            public String getHidrawUniq(int fd) {
+                return nativeGetHidrawUniq(fd);
+            }
+
+            @Override
+            public int getHidrawBusType(int fd) {
+                return nativeGetHidrawBusType(fd);
+            }
+        };
+    }
+
+    private native int nativeGetHidrawDescSize(int fd);
+
+    private native byte[] nativeGetHidrawDesc(int fd, int descSize);
+
+    private native String nativeGetHidrawUniq(int fd);
+
+    private native int nativeGetHidrawBusType(int fd);
+}
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 2e14abb..a341b4a 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -263,10 +263,6 @@
     // location settings are off, for emergency purposes, as read from the configuration files.
     final ArrayMap<String, ArraySet<String>> mAllowIgnoreLocationSettings = new ArrayMap<>();
 
-    // These are the packages that are allow-listed to be able to access camera when
-    // the camera privacy state is for driver assistance apps only.
-    final ArrayMap<String, Boolean> mAllowlistCameraPrivacy = new ArrayMap<>();
-
     // These are the action strings of broadcasts which are whitelisted to
     // be delivered anonymously even to apps which target O+.
     final ArraySet<String> mAllowImplicitBroadcasts = new ArraySet<>();
@@ -487,10 +483,6 @@
         return mAllowedAssociations;
     }
 
-    public ArrayMap<String, Boolean> getCameraPrivacyAllowlist() {
-        return mAllowlistCameraPrivacy;
-    }
-
     public ArraySet<String> getBugreportWhitelistedPackages() {
         return mBugreportWhitelistedPackages;
     }
@@ -1070,22 +1062,6 @@
                         }
                         XmlUtils.skipCurrentTag(parser);
                     } break;
-                    case "camera-privacy-allowlisted-app" : {
-                        if (allowOverrideAppRestrictions) {
-                            String pkgname = parser.getAttributeValue(null, "package");
-                            boolean isMandatory = XmlUtils.readBooleanAttribute(
-                                    parser, "mandatory", false);
-                            if (pkgname == null) {
-                                Slog.w(TAG, "<" + name + "> without package in "
-                                        + permFile + " at " + parser.getPositionDescription());
-                            } else {
-                                mAllowlistCameraPrivacy.put(pkgname, isMandatory);
-                            }
-                        } else {
-                            logNotAllowedInPartition(name, permFile, parser);
-                        }
-                        XmlUtils.skipCurrentTag(parser);
-                    } break;
                     case "allow-ignore-location-settings": {
                         if (allowOverrideAppRestrictions) {
                             String pkgname = parser.getAttributeValue(null, "package");
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 63ea7b4..5c95d43 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -106,7 +106,6 @@
 import android.content.pm.PermissionInfo;
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
-import android.hardware.SensorPrivacyManager;
 import android.hardware.camera2.CameraDevice.CAMERA_AUDIO_RESTRICTION;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -152,7 +151,6 @@
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.app.IAppOpsStartedCallback;
 import com.android.internal.app.MessageSamplingConfig;
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.compat.IPlatformCompat;
 import com.android.internal.os.Clock;
 import com.android.internal.pm.pkg.component.ParsedAttribution;
@@ -225,8 +223,6 @@
      */
     private static final int CURRENT_VERSION = 1;
 
-    private SensorPrivacyManager mSensorPrivacyManager;
-
     // Write at most every 30 minutes.
     static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000;
 
@@ -1235,7 +1231,6 @@
                         }
                     }
                 });
-        mSensorPrivacyManager = SensorPrivacyManager.getInstance(mContext);
     }
 
     @VisibleForTesting
@@ -4647,10 +4642,6 @@
         return pmi.isPackageSuspended(packageName, UserHandle.getUserId(uid));
     }
 
-    private boolean isAutomotive() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
-    }
-
     private boolean isOpRestrictedLocked(int uid, int code, String packageName,
             String attributionTag, int virtualDeviceId, @Nullable RestrictionBypass appBypass,
             boolean isCheckOp) {
@@ -4667,14 +4658,6 @@
             }
         }
 
-        if (Flags.privacyAllowlist()) {
-            if ((code == OP_CAMERA) && isAutomotive()) {
-                if (mSensorPrivacyManager.isCameraPrivacyEnabled(packageName)) {
-                    return true;
-                }
-            }
-        }
-
         int userHandle = UserHandle.getUserId(uid);
         restrictionSetCount = mOpUserRestrictions.size();
 
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index 3417f65..3b1c011 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -259,6 +259,7 @@
         /** used for VOL_ADJUST_VOL_UID,
          *           VOL_ADJUST_SUGG_VOL,
          *           VOL_ADJUST_STREAM_VOL,
+         *           VOL_SET_LE_AUDIO_VOL
          */
         VolumeEvent(int op, int stream, int val1, int val2, String caller) {
             mOp = op;
@@ -434,6 +435,8 @@
                             .set(MediaMetrics.Property.EVENT, "setLeAudioVolume")
                             .set(MediaMetrics.Property.INDEX, mVal1)
                             .set(MediaMetrics.Property.MAX_INDEX, mVal2)
+                            .set(MediaMetrics.Property.STREAM_TYPE,
+                                    AudioSystem.streamToString(mStream))
                             .record();
                     return;
                 case VOL_SET_AVRCP_VOL:
@@ -519,7 +522,8 @@
                             .append(" gain dB:").append(mVal2)
                             .toString();
                 case VOL_SET_LE_AUDIO_VOL:
-                    return new StringBuilder("setLeAudioVolume:")
+                    return new StringBuilder("setLeAudioVolume(stream:")
+                            .append(AudioSystem.streamToString(mStream))
                             .append(" index:").append(mVal1)
                             .append(" maxIndex:").append(mVal2)
                             .toString();
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index f51043d..0f3f807 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -470,7 +470,8 @@
                     + index + " volume=" + volume);
         }
         AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent(
-                AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, index, maxIndex));
+                AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, streamType, index,
+                maxIndex, /*caller=*/null));
         try {
             mLeAudio.setVolume(volume);
         } catch (Exception e) {
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index cec7a79..5d415c2 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -200,7 +200,9 @@
                 Slog.d(TAG, this + ": Starting");
             }
             mRunning = true;
-            updateBinding();
+            if (!Flags.enablePreventionOfKeepAliveRouteProviders()) {
+                updateBinding();
+            }
         }
         if (rebindIfDisconnected && mActiveConnection == null && shouldBind()) {
             unbind();
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
index 233a3ab..fcca94b 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
@@ -150,7 +150,9 @@
                     mCallback.onAddProviderService(proxy);
                 } else if (sourceIndex >= targetIndex) {
                     MediaRoute2ProviderServiceProxy proxy = mProxies.get(sourceIndex);
-                    proxy.start(/* rebindIfDisconnected= */ true); // restart the proxy if needed
+                    proxy.start(
+                            /* rebindIfDisconnected= */
+                                    !Flags.enablePreventionOfKeepAliveRouteProviders());
                     Collections.swap(mProxies, sourceIndex, targetIndex++);
                 }
             }
diff --git a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
index bbe6d3a..2cd8fe0 100644
--- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
+++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
@@ -18,10 +18,13 @@
 
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.hardware.DataSpace;
 import android.media.MediaMetrics;
+import android.media.codec.Enums;
 import android.media.metrics.BundleSession;
 import android.media.metrics.EditingEndedEvent;
 import android.media.metrics.IMediaMetricsManager;
+import android.media.metrics.MediaItemInfo;
 import android.media.metrics.NetworkEvent;
 import android.media.metrics.PlaybackErrorEvent;
 import android.media.metrics.PlaybackMetrics;
@@ -31,7 +34,9 @@
 import android.os.PersistableBundle;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.Properties;
+import android.text.TextUtils;
 import android.util.Base64;
+import android.util.Size;
 import android.util.Slog;
 import android.util.StatsEvent;
 import android.util.StatsLog;
@@ -72,7 +77,14 @@
     private static final String mMetricsId = MediaMetrics.Name.METRICS_MANAGER;
 
     private static final String FAILED_TO_GET = "failed_to_get";
+
+    private static final MediaItemInfo EMPTY_MEDIA_ITEM_INFO = new MediaItemInfo.Builder().build();
+    private static final int DURATION_BUCKETS_BELOW_ONE_MINUTE = 8;
+    private static final int DURATION_BUCKETS_COUNT = 13;
+    private static final String AUDIO_MIME_TYPE_PREFIX = "audio/";
+    private static final String VIDEO_MIME_TYPE_PREFIX = "video/";
     private final SecureRandom mSecureRandom;
+
     @GuardedBy("mLock")
     private Integer mMode = null;
     @GuardedBy("mLock")
@@ -353,6 +365,51 @@
             if (level == LOGGING_LEVEL_BLOCKED) {
                 return;
             }
+            MediaItemInfo inputMediaItemInfo =
+                    event.getInputMediaItemInfos().isEmpty()
+                            ? EMPTY_MEDIA_ITEM_INFO
+                            : event.getInputMediaItemInfos().get(0);
+            String inputAudioSampleMimeType =
+                    getFilteredFirstMimeType(
+                            inputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX);
+            String inputVideoSampleMimeType =
+                    getFilteredFirstMimeType(
+                            inputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX);
+            Size inputVideoSize = inputMediaItemInfo.getVideoSize();
+            int inputVideoResolution = getVideoResolutionEnum(inputVideoSize);
+            if (inputVideoResolution == Enums.RESOLUTION_UNKNOWN) {
+                // Try swapping width/height in case it's a portrait video.
+                inputVideoResolution =
+                        getVideoResolutionEnum(
+                                new Size(inputVideoSize.getHeight(), inputVideoSize.getWidth()));
+            }
+            List<String> inputCodecNames = inputMediaItemInfo.getCodecNames();
+            String inputFirstCodecName = !inputCodecNames.isEmpty() ? inputCodecNames.get(0) : "";
+            String inputSecondCodecName = inputCodecNames.size() > 1 ? inputCodecNames.get(1) : "";
+
+            MediaItemInfo outputMediaItemInfo =
+                    event.getOutputMediaItemInfo() == null
+                            ? EMPTY_MEDIA_ITEM_INFO
+                            : event.getOutputMediaItemInfo();
+            String outputAudioSampleMimeType =
+                    getFilteredFirstMimeType(
+                            outputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX);
+            String outputVideoSampleMimeType =
+                    getFilteredFirstMimeType(
+                            outputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX);
+            Size outputVideoSize = outputMediaItemInfo.getVideoSize();
+            int outputVideoResolution = getVideoResolutionEnum(outputVideoSize);
+            if (outputVideoResolution == Enums.RESOLUTION_UNKNOWN) {
+                // Try swapping width/height in case it's a portrait video.
+                outputVideoResolution =
+                        getVideoResolutionEnum(
+                                new Size(outputVideoSize.getHeight(), outputVideoSize.getWidth()));
+            }
+            List<String> outputCodecNames = outputMediaItemInfo.getCodecNames();
+            String outputFirstCodecName =
+                    !outputCodecNames.isEmpty() ? outputCodecNames.get(0) : "";
+            String outputSecondCodecName =
+                    outputCodecNames.size() > 1 ? outputCodecNames.get(1) : "";
             StatsEvent statsEvent =
                     StatsEvent.newBuilder()
                             .setAtomId(798)
@@ -360,6 +417,66 @@
                             .writeInt(event.getFinalState())
                             .writeInt(event.getErrorCode())
                             .writeLong(event.getTimeSinceCreatedMillis())
+                            .writeInt(getThroughputFps(event))
+                            .writeInt(event.getInputMediaItemInfos().size())
+                            .writeInt(inputMediaItemInfo.getSourceType())
+                            .writeLong(
+                                    getBucketedDurationMillis(
+                                            inputMediaItemInfo.getDurationMillis()))
+                            .writeLong(
+                                    getBucketedDurationMillis(
+                                            inputMediaItemInfo.getClipDurationMillis()))
+                            .writeString(
+                                    getFilteredMimeType(inputMediaItemInfo.getContainerMimeType()))
+                            .writeString(inputAudioSampleMimeType)
+                            .writeString(inputVideoSampleMimeType)
+                            .writeInt(getCodecEnum(inputVideoSampleMimeType))
+                            .writeInt(
+                                    getFilteredAudioSampleRateHz(
+                                            inputMediaItemInfo.getAudioSampleRateHz()))
+                            .writeInt(inputMediaItemInfo.getAudioChannelCount())
+                            .writeInt(inputVideoSize.getWidth())
+                            .writeInt(inputVideoSize.getHeight())
+                            .writeInt(inputVideoResolution)
+                            .writeInt(getVideoResolutionAspectRatioEnum(inputVideoSize))
+                            .writeInt(inputMediaItemInfo.getVideoDataSpace())
+                            .writeInt(
+                                    getVideoHdrFormatEnum(
+                                            inputMediaItemInfo.getVideoDataSpace(),
+                                            inputVideoSampleMimeType))
+                            .writeInt(Math.round(inputMediaItemInfo.getVideoFrameRate()))
+                            .writeInt(getVideoFrameRateEnum(inputMediaItemInfo.getVideoFrameRate()))
+                            .writeString(inputFirstCodecName)
+                            .writeString(inputSecondCodecName)
+                            .writeLong(
+                                    getBucketedDurationMillis(
+                                            outputMediaItemInfo.getDurationMillis()))
+                            .writeLong(
+                                    getBucketedDurationMillis(
+                                            outputMediaItemInfo.getClipDurationMillis()))
+                            .writeString(
+                                    getFilteredMimeType(outputMediaItemInfo.getContainerMimeType()))
+                            .writeString(outputAudioSampleMimeType)
+                            .writeString(outputVideoSampleMimeType)
+                            .writeInt(getCodecEnum(outputVideoSampleMimeType))
+                            .writeInt(
+                                    getFilteredAudioSampleRateHz(
+                                            outputMediaItemInfo.getAudioSampleRateHz()))
+                            .writeInt(outputMediaItemInfo.getAudioChannelCount())
+                            .writeInt(outputVideoSize.getWidth())
+                            .writeInt(outputVideoSize.getHeight())
+                            .writeInt(outputVideoResolution)
+                            .writeInt(getVideoResolutionAspectRatioEnum(outputVideoSize))
+                            .writeInt(outputMediaItemInfo.getVideoDataSpace())
+                            .writeInt(
+                                    getVideoHdrFormatEnum(
+                                            outputMediaItemInfo.getVideoDataSpace(),
+                                            outputVideoSampleMimeType))
+                            .writeInt(Math.round(outputMediaItemInfo.getVideoFrameRate()))
+                            .writeInt(
+                                    getVideoFrameRateEnum(outputMediaItemInfo.getVideoFrameRate()))
+                            .writeString(outputFirstCodecName)
+                            .writeString(outputSecondCodecName)
                             .usePooledBuffer()
                             .build();
             StatsLog.write(statsEvent);
@@ -511,4 +628,215 @@
             }
         }
     }
+
+    private static int getThroughputFps(EditingEndedEvent event) {
+        MediaItemInfo outputMediaItemInfo = event.getOutputMediaItemInfo();
+        if (outputMediaItemInfo == null) {
+            return -1;
+        }
+        long videoSampleCount = outputMediaItemInfo.getVideoSampleCount();
+        if (videoSampleCount == MediaItemInfo.VALUE_UNSPECIFIED) {
+            return -1;
+        }
+        long elapsedTimeMs = event.getTimeSinceCreatedMillis();
+        if (elapsedTimeMs == EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) {
+            return -1;
+        }
+        return (int)
+                Math.min(Integer.MAX_VALUE, Math.round(1000.0 * videoSampleCount / elapsedTimeMs));
+    }
+
+    private static long getBucketedDurationMillis(long durationMillis) {
+        if (durationMillis == MediaItemInfo.VALUE_UNSPECIFIED || durationMillis <= 0) {
+            return -1;
+        }
+        // Bucket values in an exponential distribution to reduce the precision that's stored:
+        // bucket index -> range -> bucketed duration
+        // 1 -> [0, 469 ms) -> 235 ms
+        // 2 -> [469 ms, 938 ms) -> 469 ms
+        // 3 -> [938 ms, 1875 ms) -> 938 ms
+        // 4 -> [1875 ms, 3750 ms) -> 1875 ms
+        // 5 -> [3750 ms, 7500 ms) -> 3750 ms
+        // [...]
+        // 13 -> [960000 ms, max) -> 960000 ms
+        int bucketIndex =
+                (int)
+                        Math.floor(
+                                DURATION_BUCKETS_BELOW_ONE_MINUTE
+                                        + Math.log((durationMillis + 1) / 60_000.0) / Math.log(2));
+        // Clamp to range [0, DURATION_BUCKETS_COUNT].
+        bucketIndex = Math.min(DURATION_BUCKETS_COUNT, Math.max(0, bucketIndex));
+        // Map back onto the representative value for the bucket.
+        return (long)
+                Math.ceil(Math.pow(2, bucketIndex - DURATION_BUCKETS_BELOW_ONE_MINUTE) * 60_000.0);
+    }
+
+    /**
+     * Returns the first entry in {@code mimeTypes} with the given prefix, if it matches the
+     * filtering allowlist. If no entries match the prefix or if the first matching entry is not on
+     * the allowlist, returns an empty string.
+     */
+    private static String getFilteredFirstMimeType(List<String> mimeTypes, String prefix) {
+        int size = mimeTypes.size();
+        for (int i = 0; i < size; i++) {
+            String mimeType = mimeTypes.get(i);
+            if (mimeType.startsWith(prefix)) {
+                return getFilteredMimeType(mimeType);
+            }
+        }
+        return "";
+    }
+
+    private static String getFilteredMimeType(String mimeType) {
+        if (TextUtils.isEmpty(mimeType)) {
+            return "";
+        }
+        // Discard all inputs that aren't allowlisted MIME types.
+        return switch (mimeType) {
+            case "video/mp4",
+                            "video/x-matroska",
+                            "video/webm",
+                            "video/3gpp",
+                            "video/avc",
+                            "video/hevc",
+                            "video/x-vnd.on2.vp8",
+                            "video/x-vnd.on2.vp9",
+                            "video/av01",
+                            "video/mp2t",
+                            "video/mp4v-es",
+                            "video/mpeg",
+                            "video/x-flv",
+                            "video/dolby-vision",
+                            "video/raw",
+                            "audio/mp4",
+                            "audio/mp4a-latm",
+                            "audio/x-matroska",
+                            "audio/webm",
+                            "audio/mpeg",
+                            "audio/mpeg-L1",
+                            "audio/mpeg-L2",
+                            "audio/ac3",
+                            "audio/eac3",
+                            "audio/eac3-joc",
+                            "audio/av4",
+                            "audio/true-hd",
+                            "audio/vnd.dts",
+                            "audio/vnd.dts.hd",
+                            "audio/vorbis",
+                            "audio/opus",
+                            "audio/flac",
+                            "audio/ogg",
+                            "audio/wav",
+                            "audio/midi",
+                            "audio/raw",
+                            "application/mp4",
+                            "application/webm",
+                            "application/x-matroska",
+                            "application/dash+xml",
+                            "application/x-mpegURL",
+                            "application/vnd.ms-sstr+xml" ->
+                    mimeType;
+            default -> "";
+        };
+    }
+
+    private static int getCodecEnum(String mimeType) {
+        if (TextUtils.isEmpty(mimeType)) {
+            return Enums.CODEC_UNKNOWN;
+        }
+        return switch (mimeType) {
+            case "video/avc" -> Enums.CODEC_AVC;
+            case "video/hevc" -> Enums.CODEC_HEVC;
+            case "video/x-vnd.on2.vp8" -> Enums.CODEC_VP8;
+            case "video/x-vnd.on2.vp9" -> Enums.CODEC_VP9;
+            case "video/av01" -> Enums.CODEC_AV1;
+            default -> Enums.CODEC_UNKNOWN;
+        };
+    }
+
+    private static int getFilteredAudioSampleRateHz(int sampleRateHz) {
+        return switch (sampleRateHz) {
+            case 8000, 11025, 16000, 22050, 44100, 48000, 96000, 192000 -> sampleRateHz;
+            default -> -1;
+        };
+    }
+
+    private static int getVideoResolutionEnum(Size size) {
+        int width = size.getWidth();
+        int height = size.getHeight();
+        if (width == 352 && height == 640) {
+            return Enums.RESOLUTION_352X640;
+        } else if (width == 360 && height == 640) {
+            return Enums.RESOLUTION_360X640;
+        } else if (width == 480 && height == 640) {
+            return Enums.RESOLUTION_480X640;
+        } else if (width == 480 && height == 854) {
+            return Enums.RESOLUTION_480X854;
+        } else if (width == 540 && height == 960) {
+            return Enums.RESOLUTION_540X960;
+        } else if (width == 576 && height == 1024) {
+            return Enums.RESOLUTION_576X1024;
+        } else if (width == 1280 && height == 720) {
+            return Enums.RESOLUTION_720P_HD;
+        } else if (width == 1920 && height == 1080) {
+            return Enums.RESOLUTION_1080P_FHD;
+        } else if (width == 1440 && height == 2560) {
+            return Enums.RESOLUTION_1440X2560;
+        } else if (width == 3840 && height == 2160) {
+            return Enums.RESOLUTION_4K_UHD;
+        } else if (width == 7680 && height == 4320) {
+            return Enums.RESOLUTION_8K_UHD;
+        } else {
+            return Enums.RESOLUTION_UNKNOWN;
+        }
+    }
+
+    private static int getVideoResolutionAspectRatioEnum(Size size) {
+        int width = size.getWidth();
+        int height = size.getHeight();
+        if (width <= 0 || height <= 0) {
+            return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_UNSPECIFIED;
+        } else if (width < height) {
+            return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_PORTRAIT;
+        } else if (height < width) {
+            return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_LANDSCAPE;
+        } else {
+            return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_SQUARE;
+        }
+    }
+
+    private static int getVideoHdrFormatEnum(int dataSpace, String mimeType) {
+        if (dataSpace == DataSpace.DATASPACE_UNKNOWN) {
+            return Enums.HDR_FORMAT_UNKNOWN;
+        }
+        if (mimeType.equals("video/dolby-vision")) {
+            return Enums.HDR_FORMAT_DOLBY_VISION;
+        }
+        int standard = DataSpace.getStandard(dataSpace);
+        int transfer = DataSpace.getTransfer(dataSpace);
+        if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_HLG) {
+            return Enums.HDR_FORMAT_HLG;
+        }
+        if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_ST2084) {
+            // We don't currently distinguish HDR10+ from HDR10.
+            return Enums.HDR_FORMAT_HDR10;
+        }
+        return Enums.HDR_FORMAT_NONE;
+    }
+
+    private static int getVideoFrameRateEnum(float frameRate) {
+        int frameRateInt = Math.round(frameRate);
+        return switch (frameRateInt) {
+            case 24 -> Enums.FRAMERATE_24;
+            case 25 -> Enums.FRAMERATE_25;
+            case 30 -> Enums.FRAMERATE_30;
+            case 50 -> Enums.FRAMERATE_50;
+            case 60 -> Enums.FRAMERATE_60;
+            case 120 -> Enums.FRAMERATE_120;
+            case 240 -> Enums.FRAMERATE_240;
+            case 480 -> Enums.FRAMERATE_480;
+            case 960 -> Enums.FRAMERATE_960;
+            default -> Enums.FRAMERATE_UNKNOWN;
+        };
+    }
 }
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index d0c0543..f645eaa 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -16,6 +16,7 @@
 
 package com.android.server.notification;
 
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
 import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT;
 import static android.content.Context.BIND_AUTO_CREATE;
 import static android.content.Context.BIND_FOREGROUND_SERVICE;
@@ -24,6 +25,7 @@
 import static android.os.UserHandle.USER_SYSTEM;
 import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -1802,6 +1804,8 @@
         public ComponentName component;
         public int userid;
         public boolean isSystem;
+        @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+        public boolean isSystemUi;
         public ServiceConnection connection;
         public int targetSdkVersion;
         public Pair<ComponentName, Integer> mKey;
@@ -1836,6 +1840,11 @@
             return isSystem;
         }
 
+        @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+        public boolean isSystemUi() {
+            return isSystemUi;
+        }
+
         @Override
         public String toString() {
             return new StringBuilder("ManagedServiceInfo[")
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e7ad99a..3507d2d 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -71,6 +71,7 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
 import static android.app.Flags.lifetimeExtensionRefactor;
 import static android.app.NotificationManager.zenModeFromInterruptionFilter;
 import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED;
@@ -159,6 +160,7 @@
 import android.Manifest.permission;
 import android.annotation.DurationMillisLong;
 import android.annotation.ElapsedRealtimeLong;
+import android.annotation.FlaggedApi;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1852,6 +1854,7 @@
             }
             if (ACTION_NOTIFICATION_TIMEOUT.equals(action)) {
                 final NotificationRecord record;
+                // TODO: b/323013410 - Record should be cloned instead of used directly.
                 synchronized (mNotificationLock) {
                     record = findNotificationByKeyLocked(intent.getStringExtra(EXTRA_KEY));
                 }
@@ -1864,6 +1867,14 @@
                                 FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
                                         | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
                                 true, record.getUserId(), REASON_TIMEOUT, null);
+                        // If cancellation will be prevented due to lifetime extension, we send an
+                        // update to system UI.
+                        synchronized (mNotificationLock) {
+                            maybeNotifySystemUiListenerLifetimeExtendedLocked(record,
+                                    record.getSbn().getPackageName(),
+                                    mActivityManager.getPackageImportance(
+                                            record.getSbn().getPackageName()));
+                        }
                     } else {
                         cancelNotification(record.getSbn().getUid(),
                                 record.getSbn().getInitialPid(),
@@ -3825,7 +3836,17 @@
             int mustNotHaveFlags = isCallingUidSystem() ? 0 :
                     (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_AUTOGROUP_SUMMARY);
             if (lifetimeExtensionRefactor()) {
+                // Also don't allow client apps to cancel lifetime extended notifs.
                 mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+                // If cancellation will be prevented due to lifetime extension, we send an update to
+                // system UI.
+                NotificationRecord record = null;
+                final int packageImportance = mActivityManager.getPackageImportance(pkg);
+                synchronized (mNotificationLock) {
+                    record = findNotificationLocked(pkg, tag, id, userId);
+                    maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg,
+                            packageImportance);
+                }
             }
 
             cancelNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(),
@@ -3845,6 +3866,16 @@
                         pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
                                 | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
                         userId, REASON_APP_CANCEL_ALL);
+                // If cancellation will be prevented due to lifetime extension, we send updates
+                // to system UI.
+                // In this case, we need to hold the lock to access these lists.
+                final int packageImportance = mActivityManager.getPackageImportance(pkg);
+                synchronized (mNotificationLock) {
+                    notifySystemUiListenerLifetimeExtendedListLocked(mNotificationList,
+                            packageImportance);
+                    notifySystemUiListenerLifetimeExtendedListLocked(mEnqueuedNotifications,
+                            packageImportance);
+                }
             } else {
                 cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(),
                         pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB,
@@ -4891,11 +4922,19 @@
             final long identity = Binder.clearCallingIdentity();
             boolean notificationsRapidlyCleared = false;
             final String pkg;
+            final int packageImportance;
+            final ManagedServiceInfo info;
             try {
                 synchronized (mNotificationLock) {
-                    final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
+                    info = mListeners.checkServiceTokenLocked(token);
                     pkg = info.component.getPackageName();
-
+                }
+                if (lifetimeExtensionRefactor()) {
+                    packageImportance = mActivityManager.getPackageImportance(pkg);
+                } else {
+                    packageImportance = IMPORTANCE_NONE;
+                }
+                synchronized (mNotificationLock) {
                     // Cancellation reason. If the token comes from assistant, label the
                     // cancellation as coming from the assistant; default to LISTENER_CANCEL.
                     int reason = REASON_LISTENER_CANCEL;
@@ -4917,7 +4956,7 @@
                                     || isNotificationRecent(r.getUpdateTimeMs());
                             cancelNotificationFromListenerLocked(info, callingUid, callingPid,
                                     r.getSbn().getPackageName(), r.getSbn().getTag(),
-                                    r.getSbn().getId(), userId, reason);
+                                    r.getSbn().getId(), userId, reason, packageImportance);
                         }
                     } else {
                         for (NotificationRecord notificationRecord : mNotificationList) {
@@ -4931,6 +4970,12 @@
                                     REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
                                     FLAG_ONGOING_EVENT | FLAG_NO_CLEAR
                                             | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+                            // If cancellation will be prevented due to lifetime extension, we send
+                            // an update to system UI.
+                            notifySystemUiListenerLifetimeExtendedListLocked(mNotificationList,
+                                    packageImportance);
+                            notifySystemUiListenerLifetimeExtendedListLocked(mEnqueuedNotifications,
+                                    packageImportance);
                         } else {
                             cancelAllLocked(callingUid, callingPid, info.userid,
                                     REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
@@ -5051,10 +5096,14 @@
         @GuardedBy("mNotificationLock")
         private void cancelNotificationFromListenerLocked(ManagedServiceInfo info,
                 int callingUid, int callingPid, String pkg, String tag, int id, int userId,
-                int reason) {
+                int reason, int packageImportance) {
             int mustNotHaveFlags = FLAG_ONGOING_EVENT;
             if (lifetimeExtensionRefactor()) {
                 mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+                // If cancellation will be prevented due to lifetime extension, we send an update
+                // to system UI.
+                NotificationRecord record = findNotificationLocked(pkg, tag, id, userId);
+                maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg, packageImportance);
             }
             cancelNotification(callingUid, callingPid, pkg, tag, id, 0 /* mustHaveFlags */,
                     mustNotHaveFlags,
@@ -5197,7 +5246,13 @@
             final int callingUid = Binder.getCallingUid();
             final int callingPid = Binder.getCallingPid();
             final long identity = Binder.clearCallingIdentity();
+            final int packageImportance;
             try {
+                if (lifetimeExtensionRefactor()) {
+                    packageImportance = mActivityManager.getPackageImportance(pkg);
+                } else {
+                    packageImportance = IMPORTANCE_NONE;
+                }
                 synchronized (mNotificationLock) {
                     final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
                     int cancelReason = REASON_LISTENER_CANCEL;
@@ -5210,7 +5265,7 @@
                                 + " use cancelNotification(key) instead.");
                     } else {
                         cancelNotificationFromListenerLocked(info, callingUid, callingPid,
-                                pkg, tag, id, info.userid, cancelReason);
+                                pkg, tag, id, info.userid, cancelReason, packageImportance);
                     }
                 }
             } finally {
@@ -11654,6 +11709,30 @@
         });
     }
 
+    @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+    @GuardedBy("mNotificationLock")
+    private void notifySystemUiListenerLifetimeExtendedListLocked(
+            List<NotificationRecord> notificationList, int packageImportance) {
+        for (int i = notificationList.size() - 1; i >= 0; --i) {
+            NotificationRecord record = notificationList.get(i);
+            maybeNotifySystemUiListenerLifetimeExtendedLocked(record,
+                    record.getSbn().getPackageName(), packageImportance);
+        }
+    }
+
+    @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+    @GuardedBy("mNotificationLock")
+    private void maybeNotifySystemUiListenerLifetimeExtendedLocked(NotificationRecord record,
+            String pkg, int packageImportance) {
+        if (record != null && (record.getSbn().getNotification().flags
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+            boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND;
+            mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(),
+                    record, isAppForeground,
+                    mPostNotificationTrackerFactory.newTracker(null)));
+        }
+    }
+
     public class NotificationListeners extends ManagedServices {
         static final String TAG_ENABLED_NOTIFICATION_LISTENERS = "enabled_listeners";
         static final String TAG_REQUESTED_LISTENERS = "request_listeners";
@@ -11777,6 +11856,11 @@
 
         @Override
         public void onServiceAdded(ManagedServiceInfo info) {
+            if (lifetimeExtensionRefactor()) {
+                // Only System or System UI can call registerSystemService, so if the caller is not
+                // system, we know it's system UI.
+                info.isSystemUi = !isCallerSystemOrPhone();
+            }
             final INotificationListener listener = (INotificationListener) info.service;
             final NotificationRankingUpdate update;
             synchronized (mNotificationLock) {
@@ -12141,6 +12225,23 @@
                         continue;
                     }
 
+                    if (lifetimeExtensionRefactor()) {
+                        // Checks if this is a request to notify system UI about a notification that
+                        // has been lifetime extended.
+                        // (We only need to check old for the flag, because in both cancellation and
+                        // update cases, old should have the flag.)
+                        // If it is such a request, and this is system UI, we send the post request
+                        // only to System UI, and break as we don't need to continue checking other
+                        // Managed Services.
+                        if (info.isSystemUi() && old != null && old.getNotification() != null
+                                && (old.getNotification().flags
+                                & Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+                            final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
+                            listenerCalls.add(() -> notifyPosted(info, oldSbn, update));
+                            break;
+                        }
+                    }
+
                     // If we shouldn't notify all listeners, this means the hidden state of
                     // a notification was changed.  Don't notifyPosted listeners targeting >= P.
                     // Instead, those listeners will receive notifyRankingUpdate.
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
index 3f00a9d..7d90240 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
@@ -26,6 +26,7 @@
 import android.content.pm.verify.domain.DomainVerificationManager;
 import android.content.pm.verify.domain.DomainVerificationUserState;
 import android.content.pm.verify.domain.IDomainVerificationManager;
+import android.os.Bundle;
 import android.os.ServiceSpecificException;
 
 import java.util.List;
@@ -41,6 +42,27 @@
         mService = service;
     }
 
+    @Override
+    public void setUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull Bundle domainToGroupsBundle) {
+        try {
+            mService.setUriRelativeFilterGroups(packageName, domainToGroupsBundle);
+        } catch (Exception e) {
+            throw rethrow(e);
+        }
+    }
+
+    @NonNull
+    @Override
+    public Bundle getUriRelativeFilterGroups(
+            @NonNull String packageName, @NonNull List<String> domains) {
+        try {
+            return mService.getUriRelativeFilterGroups(packageName, domains);
+        } catch (Exception e) {
+            throw rethrow(e);
+        }
+    }
+
     @NonNull
     @Override
     public List<String> queryValidVerificationPackageNames() {
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
index ac6d795..de464a4 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.content.UriRelativeFilter;
+import android.content.UriRelativeFilterGroup;
 import android.content.pm.Signature;
 import android.content.pm.verify.domain.DomainVerificationState;
 import android.os.UserHandle;
@@ -38,7 +40,10 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
 import java.util.UUID;
 import java.util.function.Function;
 
@@ -67,6 +72,13 @@
     public static final String TAG_DOMAIN = "domain";
     public static final String ATTR_NAME = "name";
     public static final String ATTR_STATE = "state";
+    public static final String TAG_URI_RELATIVE_FILTER_GROUPS = "uri-relative-filter-groups";
+    public static final String TAG_URI_RELATIVE_FILTER_GROUP = "uri-relative-filter-group";
+    public static final String ATTR_ACTION = "action";
+    public static final String TAG_URI_RELATIVE_FILTER = "uri-relative-filter";
+    public static final String ATTR_URI_PART = "uri-part";
+    public static final String ATTR_PATTERN_TYPE = "pattern-type";
+    public static final String ATTR_FILTER = "filter";
 
     /**
      * @param pkgNameToSignature Converts package name to a string representation of its signature.
@@ -176,6 +188,7 @@
 
         final ArrayMap<String, Integer> stateMap = new ArrayMap<>();
         final SparseArray<DomainVerificationInternalUserState> userStates = new SparseArray<>();
+        final ArrayMap<String, List<UriRelativeFilterGroup>> groupMap = new ArrayMap<>();
 
         SettingsXml.ChildSection child = section.children();
         while (child.moveToNext()) {
@@ -186,11 +199,47 @@
                 case TAG_USER_STATES:
                     readUserStates(child, userStates);
                     break;
+                case TAG_URI_RELATIVE_FILTER_GROUPS:
+                    readUriRelativeFilterGroups(child, groupMap);
+                    break;
             }
         }
 
         return new DomainVerificationPkgState(packageName, id, hasAutoVerifyDomains, stateMap,
-                userStates, signature);
+                userStates, signature, groupMap);
+    }
+
+    private static void readUriRelativeFilterGroups(@NonNull SettingsXml.ReadSection section,
+            @NonNull ArrayMap<String, List<UriRelativeFilterGroup>> groupMap) {
+        SettingsXml.ChildSection child = section.children();
+        while (child.moveToNext(TAG_DOMAIN)) {
+            String domain = child.getString(ATTR_NAME);
+            groupMap.put(domain, createUriRelativeFilterGroupsFromXml(child));
+        }
+    }
+
+    private static ArrayList<UriRelativeFilterGroup> createUriRelativeFilterGroupsFromXml(
+            @NonNull SettingsXml.ReadSection section) {
+        SettingsXml.ChildSection child = section.children();
+        ArrayList<UriRelativeFilterGroup> groups = new ArrayList<>();
+        while (child.moveToNext(TAG_URI_RELATIVE_FILTER_GROUP)) {
+            UriRelativeFilterGroup group = new UriRelativeFilterGroup(section.getInt(ATTR_ACTION));
+            readUriRelativeFiltersFromXml(child, group);
+            groups.add(group);
+        }
+        return groups;
+    }
+
+    private static void readUriRelativeFiltersFromXml(
+            @NonNull SettingsXml.ReadSection section, UriRelativeFilterGroup group) {
+        SettingsXml.ChildSection child = section.children();
+        while (child.moveToNext(TAG_URI_RELATIVE_FILTER)) {
+            String filter = child.getString(ATTR_FILTER);
+            if (filter != null) {
+                group.addUriRelativeFilter(new UriRelativeFilter(child.getInt(ATTR_URI_PART),
+                        child.getInt(ATTR_PATTERN_TYPE), filter));
+            }
+        }
     }
 
     private static void readUserStates(@NonNull SettingsXml.ReadSection section,
@@ -236,6 +285,7 @@
                              .attribute(ATTR_SIGNATURE, signature)) {
             writeStateMap(parentSection, pkgState.getStateMap());
             writeUserStates(parentSection, userId, pkgState.getUserStates());
+            writeUriRelativeFilterGroupMap(parentSection, pkgState.getUriRelativeFilterGroupMap());
         }
     }
 
@@ -334,6 +384,52 @@
         }
     }
 
+    private static void writeUriRelativeFilterGroupMap(
+            @NonNull SettingsXml.WriteSection parentSection,
+            @NonNull ArrayMap<String, List<UriRelativeFilterGroup>> groupMap) throws IOException {
+        if (groupMap.isEmpty()) {
+            return;
+        }
+        try (SettingsXml.WriteSection section =
+                     parentSection.startSection(TAG_URI_RELATIVE_FILTER_GROUPS)) {
+            for (int i = 0; i < groupMap.size(); i++) {
+                writeUriRelativeFilterGroups(section, groupMap.keyAt(i), groupMap.valueAt(i));
+            }
+        }
+    }
+
+    private static void writeUriRelativeFilterGroups(
+            @NonNull SettingsXml.WriteSection parentSection, @NonNull String domain,
+            @NonNull List<UriRelativeFilterGroup> groups) throws IOException {
+        if (groups.isEmpty()) {
+            return;
+        }
+        try (SettingsXml.WriteSection section =
+                     parentSection.startSection(TAG_DOMAIN)
+                             .attribute(ATTR_NAME, domain)) {
+            for (int i = 0; i < groups.size(); i++) {
+                writeUriRelativeFilterGroup(section, groups.get(i));
+            }
+        }
+    }
+
+    private static void writeUriRelativeFilterGroup(
+            @NonNull SettingsXml.WriteSection parentSection,
+            @NonNull UriRelativeFilterGroup group) throws IOException {
+        try (SettingsXml.WriteSection section =
+                     parentSection.startSection(TAG_URI_RELATIVE_FILTER_GROUP)
+                             .attribute(ATTR_ACTION, group.getAction())) {
+            Iterator<UriRelativeFilter> it = group.getUriRelativeFilters().iterator();
+            while (it.hasNext()) {
+                UriRelativeFilter filter = it.next();
+                section.startSection(TAG_URI_RELATIVE_FILTER)
+                        .attribute(ATTR_URI_PART, filter.getUriPart())
+                        .attribute(ATTR_PATTERN_TYPE, filter.getPatternType())
+                        .attribute(ATTR_FILTER, filter.getFilter()).finish();
+            }
+        }
+    }
+
     public static class ReadResult {
 
         @NonNull
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index c796b40..305b087 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -19,14 +19,19 @@
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptySet;
 
+import android.Manifest;
 import android.annotation.CheckResult;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.compat.annotation.ChangeId;
 import android.content.Context;
 import android.content.Intent;
+import android.content.UriRelativeFilterGroup;
+import android.content.UriRelativeFilterGroupParcel;
+import android.content.pm.Flags;
 import android.content.pm.IntentFilterVerificationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -38,6 +43,8 @@
 import android.content.pm.verify.domain.DomainVerificationState;
 import android.content.pm.verify.domain.DomainVerificationUserState;
 import android.content.pm.verify.domain.IDomainVerificationManager;
+import android.net.Uri;
+import android.os.Bundle;
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -223,6 +230,72 @@
         mProxy = proxy;
     }
 
+    /**
+     * Update the URI relative filter groups for a package's verified domains. All previously
+     * existing groups will be cleared before the new groups will be applied.
+     */
+    @RequiresPermission(Manifest.permission.DOMAIN_VERIFICATION_AGENT)
+    public void setUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull Bundle bundle)
+            throws NameNotFoundException {
+        getContext().enforceCallingOrSelfPermission(
+                android.Manifest.permission.DOMAIN_VERIFICATION_AGENT,
+                "Caller " + mConnection.getCallingUid() + " does not hold "
+                        + android.Manifest.permission.DOMAIN_VERIFICATION_AGENT);
+        if (bundle.isEmpty()) {
+            return;
+        }
+        synchronized (mLock) {
+            DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+            if (pkgState == null) {
+                throw DomainVerificationUtils.throwPackageUnavailable(packageName);
+            }
+            Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap =
+                    pkgState.getUriRelativeFilterGroupMap();
+            for (String domain : bundle.keySet()) {
+                ArrayList<UriRelativeFilterGroupParcel> parcels =
+                        bundle.getParcelableArrayList(domain, UriRelativeFilterGroupParcel.class);
+                domainToGroupsMap.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels));
+            }
+        }
+    }
+
+    /**
+     * Retrieve the current URI relative filter groups for a package's verified domain.
+     */
+    @NonNull
+    public Bundle getUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull List<String> domains) {
+        Bundle bundle = new Bundle();
+        synchronized (mLock) {
+            DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+            if (pkgState != null) {
+                Map<String, List<UriRelativeFilterGroup>> map =
+                        pkgState.getUriRelativeFilterGroupMap();
+                for (int i = 0; i < domains.size(); i++) {
+                    List<UriRelativeFilterGroup> groups = map.get(domains.get(i));
+                    bundle.putParcelableList(domains.get(i),
+                            UriRelativeFilterGroup.groupsToParcels(groups));
+                }
+            }
+        }
+        return bundle;
+    }
+
+    @NonNull
+    private List<UriRelativeFilterGroup> getUriRelativeFilterGroups(@NonNull String packageName,
+            @NonNull String domain) {
+        List<UriRelativeFilterGroup> groups = Collections.emptyList();
+        synchronized (mLock) {
+            DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+            if (pkgState != null) {
+                groups = pkgState.getUriRelativeFilterGroupMap().getOrDefault(domain,
+                        Collections.emptyList());
+            }
+        }
+        return groups;
+    }
+
     @NonNull
     public List<String> queryValidVerificationPackageNames() {
         mEnforcer.assertApprovedVerifier(mConnection.getCallingUid(), mProxy);
@@ -891,6 +964,8 @@
             }
 
             ArrayMap<String, Integer> oldStateMap = oldPkgState.getStateMap();
+            ArrayMap<String, List<UriRelativeFilterGroup>> oldGroups =
+                    oldPkgState.getUriRelativeFilterGroupMap();
             ArraySet<String> newAutoVerifyDomains =
                     mCollector.collectValidAutoVerifyDomains(newPkg);
             int newDomainsSize = newAutoVerifyDomains.size();
@@ -941,7 +1016,7 @@
 
             mAttachedPkgStates.put(pkgName, newDomainSetId, new DomainVerificationPkgState(
                     pkgName, newDomainSetId, hasAutoVerifyDomains, newStateMap, newUserStates,
-                    null /* signature */));
+                    null /* signature */, oldGroups));
         }
 
         if (sendBroadcast) {
@@ -1572,8 +1647,6 @@
     public Pair<List<ResolveInfo>, Integer> filterToApprovedApp(@NonNull Intent intent,
             @NonNull List<ResolveInfo> infos, @UserIdInt int userId,
             @NonNull Function<String, PackageStateInternal> pkgSettingFunction) {
-        String domain = intent.getData().getHost();
-
         // Collect valid infos
         ArrayMap<ResolveInfo, Integer> infoApprovals = new ArrayMap<>();
         int infosSize = infos.size();
@@ -1586,7 +1659,7 @@
         }
 
         // Find all approval levels
-        int highestApproval = fillMapWithApprovalLevels(infoApprovals, domain, userId,
+        int highestApproval = fillMapWithApprovalLevels(infoApprovals, intent.getData(), userId,
                 pkgSettingFunction);
         if (highestApproval <= APPROVAL_LEVEL_NONE) {
             return Pair.create(emptyList(), highestApproval);
@@ -1623,12 +1696,23 @@
         return Pair.create(finalList, highestApproval);
     }
 
+    private boolean matchUriRelativeFilterGroups(Uri uri, String pkgName) {
+        if (uri.getHost() == null) {
+            return false;
+        }
+        List<UriRelativeFilterGroup> groups = getUriRelativeFilterGroups(pkgName, uri.getHost());
+        if (groups.isEmpty()) {
+            return true;
+        }
+        return UriRelativeFilterGroup.matchGroupsToUri(groups, uri);
+    }
+
     /**
      * @return highest approval level found
      */
     @ApprovalLevel
     private int fillMapWithApprovalLevels(@NonNull ArrayMap<ResolveInfo, Integer> inputMap,
-            @NonNull String domain, @UserIdInt int userId,
+            @NonNull Uri uri, @UserIdInt int userId,
             @NonNull Function<String, PackageStateInternal> pkgSettingFunction) {
         int highestApproval = APPROVAL_LEVEL_NONE;
         int size = inputMap.size();
@@ -1641,12 +1725,13 @@
             ResolveInfo info = inputMap.keyAt(index);
             final String packageName = info.getComponentInfo().packageName;
             PackageStateInternal pkgSetting = pkgSettingFunction.apply(packageName);
-            if (pkgSetting == null) {
+            if (pkgSetting == null || (Flags.relativeReferenceIntentFilters()
+                    && !matchUriRelativeFilterGroups(uri, packageName))) {
                 fillInfoMapForSamePackage(inputMap, packageName, APPROVAL_LEVEL_NONE);
                 continue;
             }
-            int approval = approvalLevelForDomain(pkgSetting, domain, false, userId, DEBUG_APPROVAL,
-                    domain);
+            int approval = approvalLevelForDomain(pkgSetting, uri.getHost(), false, userId,
+                    DEBUG_APPROVAL, uri.getHost());
             highestApproval = Math.max(highestApproval, approval);
             fillInfoMapForSamePackage(inputMap, packageName, approval);
         }
diff --git a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
index d71dbbb..46051fe 100644
--- a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
+++ b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.content.UriRelativeFilterGroup;
 import android.content.pm.Signature;
 import android.content.pm.verify.domain.DomainVerificationState;
 import android.util.ArrayMap;
@@ -26,6 +27,7 @@
 
 import com.android.internal.util.DataClass;
 
+import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
 
@@ -77,15 +79,30 @@
     @Nullable
     private final String mBackupSignatureHash;
 
+    /**
+     * List of {@link UriRelativeFilterGroup} for filtering domains.
+     */
+    @NonNull
+    private final ArrayMap<String, List<UriRelativeFilterGroup>> mUriRelativeFilterGroupMap;
+
     public DomainVerificationPkgState(@NonNull String packageName, @NonNull UUID id,
             boolean hasAutoVerifyDomains) {
-        this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0), null);
+        this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0), null,
+                new ArrayMap<>());
     }
 
     public DomainVerificationPkgState(@NonNull DomainVerificationPkgState pkgState,
             @NonNull UUID id, boolean hasAutoVerifyDomains) {
         this(pkgState.getPackageName(), id, hasAutoVerifyDomains, pkgState.getStateMap(),
-                pkgState.getUserStates(), null);
+                pkgState.getUserStates(), null, new ArrayMap<>());
+    }
+
+    public DomainVerificationPkgState(@NonNull String packageName, @NonNull UUID id,
+            boolean hasAutoVerifyDomains, @NonNull ArrayMap<String, Integer> stateMap,
+            @NonNull SparseArray<DomainVerificationInternalUserState> userStates,
+            @Nullable String backupSignatureHash) {
+        this(packageName, id, hasAutoVerifyDomains, stateMap, userStates, backupSignatureHash,
+                new ArrayMap<>());
     }
 
     @Nullable
@@ -158,6 +175,8 @@
      *
      *   It's assumed the domain verification agent will eventually re-verify this domain
      *   and revoke if necessary.
+     * @param uriRelativeFilterGroupMap
+     *   List of {@link UriRelativeFilterGroup} for filtering domains.
      */
     @DataClass.Generated.Member
     public DomainVerificationPkgState(
@@ -166,7 +185,8 @@
             boolean hasAutoVerifyDomains,
             @NonNull ArrayMap<String,Integer> stateMap,
             @NonNull SparseArray<DomainVerificationInternalUserState> userStates,
-            @Nullable String backupSignatureHash) {
+            @Nullable String backupSignatureHash,
+            @NonNull ArrayMap<String,List<UriRelativeFilterGroup>> uriRelativeFilterGroupMap) {
         this.mPackageName = packageName;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mPackageName);
@@ -181,6 +201,9 @@
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mUserStates);
         this.mBackupSignatureHash = backupSignatureHash;
+        this.mUriRelativeFilterGroupMap = uriRelativeFilterGroupMap;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mUriRelativeFilterGroupMap);
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -239,6 +262,14 @@
         return mBackupSignatureHash;
     }
 
+    /**
+     * List of {@link UriRelativeFilterGroup} for filtering domains.
+     */
+    @DataClass.Generated.Member
+    public @NonNull ArrayMap<String,List<UriRelativeFilterGroup>> getUriRelativeFilterGroupMap() {
+        return mUriRelativeFilterGroupMap;
+    }
+
     @Override
     @DataClass.Generated.Member
     public String toString() {
@@ -251,7 +282,8 @@
                 "hasAutoVerifyDomains = " + mHasAutoVerifyDomains + ", " +
                 "stateMap = " + mStateMap + ", " +
                 "userStates = " + mUserStates + ", " +
-                "backupSignatureHash = " + mBackupSignatureHash +
+                "backupSignatureHash = " + mBackupSignatureHash + ", " +
+                "uriRelativeFilterGroupMap = " + mUriRelativeFilterGroupMap +
         " }";
     }
 
@@ -273,7 +305,8 @@
                 && mHasAutoVerifyDomains == that.mHasAutoVerifyDomains
                 && Objects.equals(mStateMap, that.mStateMap)
                 && userStatesEquals(that.mUserStates)
-                && Objects.equals(mBackupSignatureHash, that.mBackupSignatureHash);
+                && Objects.equals(mBackupSignatureHash, that.mBackupSignatureHash)
+                && Objects.equals(mUriRelativeFilterGroupMap, that.mUriRelativeFilterGroupMap);
     }
 
     @Override
@@ -289,14 +322,15 @@
         _hash = 31 * _hash + Objects.hashCode(mStateMap);
         _hash = 31 * _hash + userStatesHashCode();
         _hash = 31 * _hash + Objects.hashCode(mBackupSignatureHash);
+        _hash = 31 * _hash + Objects.hashCode(mUriRelativeFilterGroupMap);
         return _hash;
     }
 
     @DataClass.Generated(
-            time = 1617315369614L,
+            time = 1707351734724L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java",
-            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate @android.annotation.NonNull java.util.UUID mId\nprivate final  boolean mHasAutoVerifyDomains\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.Integer> mStateMap\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mBackupSignatureHash\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getUserState(int)\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getOrCreateUserState(int)\npublic  void removeUser(int)\npublic  void removeAllUsers()\nprivate  int userStatesHashCode()\nprivate  boolean userStatesEquals(android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState>)\nclass DomainVerificationPkgState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)")
+            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.util.UUID mId\nprivate final  boolean mHasAutoVerifyDomains\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.Integer> mStateMap\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mBackupSignatureHash\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>> mUriRelativeFilterGroupMap\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getUserState(int)\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getOrCreateUserState(int)\npublic  void removeUser(int)\npublic  void removeAllUsers()\nprivate  int userStatesHashCode()\nprivate  boolean userStatesEquals(android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState>)\nclass DomainVerificationPkgState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)")
     @Deprecated
     private void __metadata() {}
 
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 25e749f..00036e4 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -137,7 +137,6 @@
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
-import com.android.net.module.util.NetworkCapabilitiesUtils;
 import com.android.server.power.optimization.Flags;
 import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
 
@@ -1716,14 +1715,101 @@
         return mMaxLearnedBatteryCapacityUah;
     }
 
+    public class FrameworkStatsLogger {
+        public void uidProcessStateChanged(int uid, int state) {
+            // TODO(b/155216561): It is possible for isolated uids to be in a higher
+            // state than its parent uid. We should track the highest state within the union of host
+            // and isolated uids rather than only the parent uid.
+            FrameworkStatsLog.write(FrameworkStatsLog.UID_PROCESS_STATE_CHANGED, uid,
+                    ActivityManager.processStateAmToProto(state));
+        }
+
+        public void wakelockStateChanged(int uid, WorkChain wc, String name, int type,
+                int procState, boolean acquired) {
+            int event = acquired
+                    ? FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE
+                    : FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE;
+            if (wc != null) {
+                FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(),
+                        wc.getTags(), getPowerManagerWakeLockLevel(type), name,
+                        event, procState);
+            } else {
+                FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED,
+                        mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name,
+                        event, procState);
+            }
+        }
+
+        public void kernelWakeupReported(long deltaUptimeUs) {
+            FrameworkStatsLog.write(FrameworkStatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason,
+                    /* duration_usec */ deltaUptimeUs, mLastWakeupElapsedTimeMs);
+        }
+
+        public void gpsScanStateChanged(int uid, WorkChain workChain, boolean stateOn) {
+            int event = stateOn
+                    ? FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON
+                    : FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF;
+            if (workChain != null) {
+                FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
+                        workChain.getUids(), workChain.getTags(), event);
+            } else {
+                FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
+                        uid, null, event);
+            }
+        }
+
+        public void batterySaverModeChanged(boolean enabled) {
+            FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED,
+                    enabled
+                            ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON
+                            : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF);
+        }
+
+        public void deviceIdlingModeStateChanged(int mode) {
+            FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLING_MODE_STATE_CHANGED, mode);
+        }
+
+        public void deviceIdleModeStateChanged(int mode) {
+            FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode);
+        }
+
+        public void chargingStateChanged(int status) {
+            FrameworkStatsLog.write(FrameworkStatsLog.CHARGING_STATE_CHANGED, status);
+        }
+
+        public void pluggedStateChanged(int plugType) {
+            FrameworkStatsLog.write(FrameworkStatsLog.PLUGGED_STATE_CHANGED, plugType);
+        }
+
+        public void batteryLevelChanged(int level) {
+            FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_LEVEL_CHANGED, level);
+        }
+
+        public void phoneServiceStateChanged(int state, int simState, int strengthBin) {
+            FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state,
+                    simState, strengthBin);
+        }
+
+        public void phoneSignalStrengthChanged(int strengthBin) {
+            FrameworkStatsLog.write(
+                    FrameworkStatsLog.PHONE_SIGNAL_STRENGTH_CHANGED, strengthBin);
+        }
+    }
+
+    private final FrameworkStatsLogger mFrameworkStatsLogger;
+
     @VisibleForTesting
     public BatteryStatsImpl(Clock clock, File historyDirectory, @NonNull Handler handler,
-            @NonNull PowerStatsUidResolver powerStatsUidResolver) {
+            @NonNull PowerStatsUidResolver powerStatsUidResolver,
+            @NonNull FrameworkStatsLogger frameworkStatsLogger,
+            @NonNull BatteryStatsHistory.TraceDelegate traceDelegate,
+            @NonNull BatteryStatsHistory.EventLogger eventLogger) {
         mClock = clock;
         initKernelStatsReaders();
         mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
         mHandler = handler;
         mPowerStatsUidResolver = powerStatsUidResolver;
+        mFrameworkStatsLogger = frameworkStatsLogger;
         mConstants = new Constants(mHandler);
         mStartClockTimeMs = clock.currentTimeMillis();
         mDailyFile = null;
@@ -1732,12 +1818,14 @@
             mCheckinFile = null;
             mStatsFile = null;
             mHistory = new BatteryStatsHistory(mConstants.MAX_HISTORY_FILES,
-                    mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock);
+                    mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock,
+                    traceDelegate, eventLogger);
         } else {
             mCheckinFile = new AtomicFile(new File(historyDirectory, "batterystats-checkin.bin"));
             mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
             mHistory = new BatteryStatsHistory(historyDirectory, mConstants.MAX_HISTORY_FILES,
-                    mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock);
+                    mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock,
+                    traceDelegate, eventLogger);
         }
         mPlatformIdleStateCallback = null;
         mEnergyConsumerRetriever = null;
@@ -4269,7 +4357,7 @@
     }
 
     @GuardedBy("this")
-    private void updateBatteryPropertiesLocked() {
+    protected void updateBatteryPropertiesLocked() {
         try {
             IBatteryPropertiesRegistrar registrar = IBatteryPropertiesRegistrar.Stub.asInterface(
                     ServiceManager.getService("batteryproperties"));
@@ -4403,11 +4491,7 @@
                 return;
             }
         }
-        // TODO(b/155216561): It is possible for isolated uids to be in a higher
-        // state than its parent uid. We should track the highest state within the union of host
-        // and isolated uids rather than only the parent uid.
-        FrameworkStatsLog.write(FrameworkStatsLog.UID_PROCESS_STATE_CHANGED, uid,
-                ActivityManager.processStateAmToProto(state));
+        mFrameworkStatsLogger.uidProcessStateChanged(uid, state);
         getUidStatsLocked(parentUid, elapsedRealtimeMs, uptimeMs)
                 .updateUidProcessStateLocked(state, elapsedRealtimeMs, uptimeMs);
     }
@@ -4721,17 +4805,8 @@
             Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs);
             uidStats.noteStartWakeLocked(pid, name, type, elapsedRealtimeMs);
 
-            int procState = uidStats.mProcessState;
-
-            if (wc != null) {
-                FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(),
-                        wc.getTags(), getPowerManagerWakeLockLevel(type), name,
-                        FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE, procState);
-            } else {
-                FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED,
-                        mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name,
-                        FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE, procState);
-            }
+            mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name, type,
+                    uidStats.mProcessState, true /* acquired */);
         }
     }
 
@@ -4774,16 +4849,8 @@
             Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs);
             uidStats.noteStopWakeLocked(pid, name, type, elapsedRealtimeMs);
 
-            int procState = uidStats.mProcessState;
-            if (wc != null) {
-                FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(),
-                        wc.getTags(), getPowerManagerWakeLockLevel(type), name,
-                        FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE, procState);
-            } else {
-                FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED,
-                        mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name,
-                        FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE, procState);
-            }
+            mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name, type,
+                    uidStats.mProcessState, false /* acquired */);
 
             if (mappedUid != uid) {
                 // Decrement the ref count for the isolated uid and delete the mapping if uneeded.
@@ -5020,8 +5087,7 @@
             long deltaUptimeMs = uptimeMs - mLastWakeupUptimeMs;
             SamplingTimer timer = getWakeupReasonTimerLocked(mLastWakeupReason);
             timer.add(deltaUptimeMs * 1000, 1, elapsedRealtimeMs); // time in in microseconds
-            FrameworkStatsLog.write(FrameworkStatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason,
-                    /* duration_usec */ deltaUptimeMs * 1000, mLastWakeupElapsedTimeMs);
+            mFrameworkStatsLogger.kernelWakeupReported(deltaUptimeMs * 1000);
             mLastWakeupReason = null;
         }
     }
@@ -5159,14 +5225,7 @@
         }
         mGpsNesting++;
 
-        if (workChain == null) {
-            FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
-                    mapIsolatedUid(uid), null, FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON);
-        } else {
-            FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
-                    workChain.getUids(), workChain.getTags(),
-                    FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON);
-        }
+        mFrameworkStatsLogger.gpsScanStateChanged(mapIsolatedUid(uid), workChain, /* on */true);
 
         getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs).noteStartGps(elapsedRealtimeMs);
     }
@@ -5188,14 +5247,7 @@
             mGpsSignalQualityBin = -1;
         }
 
-        if (workChain == null) {
-            FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
-                    mapIsolatedUid(uid), null,
-                    FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF);
-        } else {
-            FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED, workChain.getUids(),
-                    workChain.getTags(), FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF);
-        }
+        mFrameworkStatsLogger.gpsScanStateChanged(mapIsolatedUid(uid), workChain, /* on */ false);
 
         getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs).noteStopGps(elapsedRealtimeMs);
     }
@@ -5673,10 +5725,7 @@
         } else {
             // Log an initial value for BATTERY_SAVER_MODE_STATE_CHANGED in order to
             // allow the atom to read all future state changes.
-            FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED,
-                    enabled
-                        ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON
-                        : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF);
+            mFrameworkStatsLogger.batterySaverModeChanged(enabled);
         }
     }
 
@@ -5696,10 +5745,7 @@
                         HistoryItem.STATE2_POWER_SAVE_FLAG);
                 mPowerSaveModeEnabledTimer.stopRunningLocked(elapsedRealtimeMs);
             }
-            FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED,
-                    enabled
-                        ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON
-                        : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF);
+            mFrameworkStatsLogger.batterySaverModeChanged(enabled);
         }
     }
 
@@ -5727,7 +5773,7 @@
             if (nowIdling)           statsmode = DEVICE_IDLE_MODE_DEEP;
             else if (nowLightIdling) statsmode = DEVICE_IDLE_MODE_LIGHT;
             else                     statsmode = DEVICE_IDLE_MODE_OFF;
-            FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLING_MODE_STATE_CHANGED, statsmode);
+            mFrameworkStatsLogger.deviceIdlingModeStateChanged(statsmode);
         }
         if (mDeviceIdling != nowIdling) {
             mDeviceIdling = nowIdling;
@@ -5769,7 +5815,7 @@
                 mDeviceIdleModeFullTimer.startRunningLocked(elapsedRealtimeMs);
             }
             mDeviceIdleMode = mode;
-            FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode);
+            mFrameworkStatsLogger.deviceIdleModeStateChanged(mode);
         }
     }
 
@@ -5933,8 +5979,7 @@
                 addStateFlag = HistoryItem.STATE_PHONE_SCANNING_FLAG;
                 newHistory = true;
                 mPhoneSignalScanningTimer.startRunningLocked(elapsedRealtimeMs);
-                FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state,
-                        simState, strengthBin);
+                mFrameworkStatsLogger.phoneServiceStateChanged(state, simState, strengthBin);
             }
         }
 
@@ -5944,8 +5989,7 @@
                 removeStateFlag = HistoryItem.STATE_PHONE_SCANNING_FLAG;
                 newHistory = true;
                 mPhoneSignalScanningTimer.stopRunningLocked(elapsedRealtimeMs);
-                FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state,
-                        simState, strengthBin);
+                mFrameworkStatsLogger.phoneServiceStateChanged(state, simState, strengthBin);
             }
         }
 
@@ -5966,8 +6010,7 @@
                 }
                 newSignalStrength = strengthBin;
                 newHistory = true;
-                FrameworkStatsLog.write(
-                        FrameworkStatsLog.PHONE_SIGNAL_STRENGTH_CHANGED, strengthBin);
+                mFrameworkStatsLogger.phoneSignalStrengthChanged(strengthBin);
             } else {
                 stopAllPhoneSignalStrengthTimersLocked(-1, elapsedRealtimeMs);
             }
@@ -6076,7 +6119,7 @@
         // Unknown is included in DATA_CONNECTION_OTHER.
         int bin = DATA_CONNECTION_OUT_OF_SERVICE;
         if (hasData) {
-            if (dataType > 0 && dataType <= TelephonyManager.getAllNetworkTypes().length) {
+            if (dataType > 0 && dataType <= NUM_ALL_NETWORK_TYPES) {
                 bin = dataType;
             } else {
                 switch (serviceType) {
@@ -6995,7 +7038,7 @@
     /** @hide */
     public void noteNetworkInterfaceForTransports(String iface, int[] transportTypes) {
         if (TextUtils.isEmpty(iface)) return;
-        final int displayTransport = NetworkCapabilitiesUtils.getDisplayTransport(transportTypes);
+        final int displayTransport = getDisplayTransport(transportTypes);
 
         synchronized (mModemNetworkLock) {
             if (displayTransport == TRANSPORT_CELLULAR) {
@@ -10507,8 +10550,7 @@
                 long elapsedRealtimeMs, long uptimeMs) {
             int uidRunningState;
             // Make special note of Foreground Services
-            final boolean userAwareService =
-                    (ActivityManager.isForegroundService(procState));
+            final boolean userAwareService = ActivityManager.isForegroundService(procState);
             uidRunningState = BatteryStats.mapToInternalProcessState(procState);
 
             if (mProcessState == uidRunningState && userAwareService == mInForegroundService) {
@@ -10912,6 +10954,7 @@
         mPowerProfile = powerProfile;
         mCpuScalingPolicies = cpuScalingPolicies;
         mPowerStatsUidResolver = powerStatsUidResolver;
+        mFrameworkStatsLogger = new FrameworkStatsLogger();
 
         initPowerProfile();
 
@@ -10966,7 +11009,7 @@
 
         // Notify statsd that the system is initially not in doze.
         mDeviceIdleMode = DEVICE_IDLE_MODE_OFF;
-        FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mDeviceIdleMode);
+        mFrameworkStatsLogger.deviceIdleModeStateChanged(mDeviceIdleMode);
     }
 
     private void recordPowerStats(PowerStats stats) {
@@ -11702,7 +11745,9 @@
             mWakeupReasonStats.clear();
         }
 
-        mTmpRailStats.reset();
+        if (mTmpRailStats != null) {
+            mTmpRailStats.reset();
+        }
 
         EnergyConsumerStats.resetIfNotNull(mGlobalEnergyConsumerStats);
 
@@ -11864,6 +11909,78 @@
         return networkStatsManager.getWifiUidStats();
     }
 
+    private static class NetworkStatsDelta {
+        int mUid;
+        int mSet;
+        long mRxBytes;
+        long mRxPackets;
+        long mTxBytes;
+        long mTxPackets;
+
+        public int getUid() {
+            return mUid;
+        }
+
+
+        public int getSet() {
+            return mSet;
+        }
+
+        public long getRxBytes() {
+            return mRxBytes;
+        }
+
+        public long getRxPackets() {
+            return mRxPackets;
+        }
+
+        public long getTxBytes() {
+            return mTxBytes;
+        }
+
+        public long getTxPackets() {
+            return mTxPackets;
+        }
+    }
+
+    private List<NetworkStatsDelta> computeDelta(NetworkStats currentStats,
+            NetworkStats lastStats) {
+        List<NetworkStatsDelta> deltaList = new ArrayList<>();
+        for (NetworkStats.Entry entry : currentStats) {
+            NetworkStatsDelta delta = new NetworkStatsDelta();
+            delta.mUid = entry.getUid();
+            delta.mSet = entry.getSet();
+            NetworkStats.Entry lastEntry = null;
+            if (lastStats != null) {
+                for (NetworkStats.Entry e : lastStats) {
+                    if (e.getUid() == entry.getUid() && e.getSet() == entry.getSet()
+                            && e.getTag() == entry.getTag()
+                            && e.getMetered() == entry.getMetered()
+                            && e.getRoaming() == entry.getRoaming()
+                            && e.getDefaultNetwork() == entry.getDefaultNetwork()
+                            /*&& Objects.equals(e.getIface(), entry.getIface())*/) {
+                        lastEntry = e;
+                        break;
+                    }
+                }
+            }
+            if (lastEntry != null) {
+                delta.mRxBytes = entry.getRxBytes() - lastEntry.getRxBytes();
+                delta.mRxPackets = entry.getRxPackets() - lastEntry.getRxPackets();
+                delta.mTxBytes = entry.getTxBytes() - lastEntry.getTxBytes();
+                delta.mTxPackets = entry.getTxPackets() - lastEntry.getTxPackets();
+            } else {
+                delta.mRxBytes = entry.getRxBytes();
+                delta.mRxPackets = entry.getRxPackets();
+                delta.mTxBytes = entry.getTxBytes();
+                delta.mTxPackets = entry.getTxPackets();
+            }
+            deltaList.add(delta);
+        }
+
+        return deltaList;
+    }
+
     /**
      * Distribute WiFi energy info and network traffic to apps.
      * @param info The energy information from the WiFi controller.
@@ -11879,14 +11996,14 @@
         }
 
         // Grab a separate lock to acquire the network stats, which may do I/O.
-        NetworkStats delta = null;
+        List<NetworkStatsDelta> delta;
         synchronized (mWifiNetworkLock) {
             final NetworkStats latestStats = readWifiNetworkStatsLocked(networkStatsManager);
             if (latestStats != null) {
-                delta = mLastWifiNetworkStats != null
-                        ? latestStats.subtract(mLastWifiNetworkStats)
-                        : latestStats.subtract(new NetworkStats(0, -1));
+                delta = computeDelta(latestStats, mLastWifiNetworkStats);
                 mLastWifiNetworkStats = latestStats;
+            } else {
+                delta = null;
             }
         }
 
@@ -11914,7 +12031,7 @@
             long totalTxPackets = 0;
             long totalRxPackets = 0;
             if (delta != null) {
-                for (NetworkStats.Entry entry : delta) {
+                for (NetworkStatsDelta entry : delta) {
                     if (DEBUG_ENERGY) {
                         Slog.d(TAG, "Wifi uid " + entry.getUid()
                                 + ": delta rx=" + entry.getRxBytes()
@@ -12199,13 +12316,16 @@
                 }
                 // Converting uWs to mAms.
                 // Conversion: (uWs * (1000ms / 1s) * (1mW / 1000uW)) / mV = mAms
-                long monitoredRailChargeConsumedMaMs =
-                        (long) (mTmpRailStats.getWifiTotalEnergyUseduWs() / opVolt);
+                long monitoredRailChargeConsumedMaMs = mTmpRailStats != null
+                        ? (long) (mTmpRailStats.getWifiTotalEnergyUseduWs() / opVolt)
+                        : 0L;
                 mWifiActivity.getMonitoredRailChargeConsumedMaMs().addCountLocked(
                         monitoredRailChargeConsumedMaMs);
                 mHistory.recordWifiConsumedCharge(elapsedRealtimeMs, uptimeMs,
                         (monitoredRailChargeConsumedMaMs / MILLISECONDS_IN_HOUR));
-                mTmpRailStats.resetWifiTotalEnergyUsed();
+                if (mTmpRailStats != null) {
+                    mTmpRailStats.resetWifiTotalEnergyUsed();
+                }
 
                 if (uidEstimatedConsumptionMah != null) {
                     totalEstimatedConsumptionMah = Math.max(controllerMaMs / MILLISECONDS_IN_HOUR,
@@ -13519,14 +13639,16 @@
             }
         }
 
-        // Record whether we've seen a non-zero time (for debugging b/22716723).
-        if (wakelockStats.isEmpty()) {
-            Slog.wtf(TAG, "All kernel wakelocks had time of zero");
-        }
+        if (DEBUG) {
+            // Record whether we've seen a non-zero time (for debugging b/22716723).
+            if (wakelockStats.isEmpty()) {
+                Slog.wtf(TAG, "All kernel wakelocks had time of zero");
+            }
 
-        if (numWakelocksSetStale == mKernelWakelockStats.size()) {
-            Slog.wtf(TAG, "All kernel wakelocks were set stale. new version=" +
-                    wakelockStats.kernelWakelockVersion);
+            if (numWakelocksSetStale == mKernelWakelockStats.size()) {
+                Slog.wtf(TAG, "All kernel wakelocks were set stale. new version="
+                        + wakelockStats.kernelWakelockVersion);
+            }
         }
     }
 
@@ -14711,13 +14833,13 @@
     // Inform StatsLog of setBatteryState changes.
     private void reportChangesToStatsLog(final int status, final int plugType, final int level) {
         if (!mHaveBatteryLevel || mBatteryStatus != status) {
-            FrameworkStatsLog.write(FrameworkStatsLog.CHARGING_STATE_CHANGED, status);
+            mFrameworkStatsLogger.chargingStateChanged(status);
         }
         if (!mHaveBatteryLevel || mBatteryPlugType != plugType) {
-            FrameworkStatsLog.write(FrameworkStatsLog.PLUGGED_STATE_CHANGED, plugType);
+            mFrameworkStatsLogger.pluggedStateChanged(plugType);
         }
         if (!mHaveBatteryLevel || mBatteryLevel != level) {
-            FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_LEVEL_CHANGED, level);
+            mFrameworkStatsLogger.batteryLevelChanged(level);
         }
     }
 
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
index aba8e5f..1050e8a 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
@@ -32,6 +32,7 @@
 
 import java.util.ArrayList;
 
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class MobileRadioPowerCalculator extends PowerCalculator {
     private static final String TAG = "MobRadioPowerCalculator";
     private static final boolean DEBUG = PowerCalculator.DEBUG;
@@ -320,7 +321,7 @@
 
     private double calculateActiveModemPowerMah(BatteryStats bs, long elapsedRealtimeUs) {
         final long elapsedRealtimeMs = elapsedRealtimeUs / 1000;
-        final int txLvlCount = CellSignalStrength.getNumSignalStrengthLevels();
+        final int txLvlCount = NUM_SIGNAL_STRENGTH_LEVELS;
         double consumptionMah = 0.0;
 
         if (DEBUG) {
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 64cfc8d4..59766ec 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -45,11 +45,6 @@
 import static android.hardware.SensorPrivacyManager.Sources.QS_TILE;
 import static android.hardware.SensorPrivacyManager.Sources.SETTINGS;
 import static android.hardware.SensorPrivacyManager.Sources.SHELL;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
-import static android.hardware.SensorPrivacyManager.StateTypes.DISABLED;
-import static android.hardware.SensorPrivacyManager.StateTypes.ENABLED;
 import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_HARDWARE;
 import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE;
 import static android.os.UserHandle.USER_NULL;
@@ -57,9 +52,6 @@
 
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__CAMERA;
@@ -71,11 +63,8 @@
 import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__SOURCE_UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.write;
 
-import android.Manifest;
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
@@ -98,7 +87,6 @@
 import android.content.res.Configuration;
 import android.database.ContentObserver;
 import android.graphics.drawable.Icon;
-import android.hardware.CameraPrivacyAllowlistEntry;
 import android.hardware.ISensorPrivacyListener;
 import android.hardware.ISensorPrivacyManager;
 import android.hardware.SensorPrivacyManager;
@@ -135,7 +123,6 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
@@ -144,7 +131,6 @@
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
-import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
 
@@ -153,7 +139,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 
@@ -169,24 +154,7 @@
             SensorPrivacyService.class.getName() + ".action.disable_sensor_privacy";
 
     public static final int REMINDER_DIALOG_DELAY_MILLIS = 500;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__TOGGLE_ON =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__TOGGLE_OFF =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF;
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    private static final int ACTION__ACTION_UNKNOWN =
-            PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN;
+
     private final Context mContext;
     private final SensorPrivacyServiceImpl mSensorPrivacyServiceImpl;
     private final UserManagerInternal mUserManagerInternal;
@@ -208,9 +176,6 @@
     private CallStateHelper mCallStateHelper;
     private KeyguardManager mKeyguardManager;
 
-    List<CameraPrivacyAllowlistEntry> mCameraPrivacyAllowlist =
-            new ArrayList<CameraPrivacyAllowlistEntry>();
-
     private int mCurrentUser = USER_NULL;
 
     public SensorPrivacyService(Context context) {
@@ -227,15 +192,6 @@
         mPackageManagerInternal = getLocalService(PackageManagerInternal.class);
         mNotificationManager = mContext.getSystemService(NotificationManager.class);
         mSensorPrivacyServiceImpl = new SensorPrivacyServiceImpl();
-        ArrayMap<String, Boolean> cameraPrivacyAllowlist =
-                SystemConfig.getInstance().getCameraPrivacyAllowlist();
-
-        for (Map.Entry<String, Boolean> entry : cameraPrivacyAllowlist.entrySet()) {
-            CameraPrivacyAllowlistEntry ent = new CameraPrivacyAllowlistEntry();
-            ent.packageName = entry.getKey();
-            ent.isMandatory =  entry.getValue();
-            mCameraPrivacyAllowlist.add(ent);
-        }
     }
 
     @Override
@@ -368,15 +324,8 @@
                     mHandler, mHandler::handleSensorPrivacyChanged);
             mSensorPrivacyStateController.setSensorPrivacyListener(
                     mHandler,
-                    (toggleType, userId, sensor, state) -> {
-                        mHandler.handleSensorPrivacyChanged(
-                                userId, toggleType, sensor, state.isEnabled());
-                        if (Flags.privacyAllowlist()) {
-                            mHandler.handleSensorPrivacyChanged(
-                                    userId, toggleType, sensor, state.getState());
-                        }
-                    });
-
+                    (toggleType, userId, sensor, state) -> mHandler.handleSensorPrivacyChanged(
+                            userId, toggleType, sensor, state.isEnabled()));
         }
 
         // If sensor privacy is enabled for a sensor, but the device doesn't support sensor privacy
@@ -451,15 +400,9 @@
          * @param packageName The package name of the app using the sensor
          * @param sensor The sensor that is attempting to be used
          */
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
         private void onSensorUseStarted(int uid, String packageName, int sensor) {
             UserHandle user = UserHandle.of(mCurrentUser);
-
-            if (Flags.privacyAllowlist() && (sensor == CAMERA) && isAutomotive(mContext)) {
-                if (!isCameraPrivacyEnabled(packageName)) {
-                    return;
-                }
-            } else if (!isCombinedToggleSensorPrivacyEnabled(sensor)) {
+            if (!isCombinedToggleSensorPrivacyEnabled(sensor)) {
                 return;
             }
 
@@ -784,12 +727,6 @@
                     == Configuration.UI_MODE_TYPE_TELEVISION;
         }
 
-        private boolean isAutomotive(Context context) {
-            int uiMode = context.getResources().getConfiguration().uiMode;
-            return (uiMode & Configuration.UI_MODE_TYPE_MASK)
-                    == Configuration.UI_MODE_TYPE_CAR;
-        }
-
         /**
          * Sets the sensor privacy to the provided state and notifies all listeners of the new
          * state.
@@ -829,225 +766,6 @@
             setToggleSensorPrivacyUnchecked(TOGGLE_TYPE_SOFTWARE, userId, source, sensor, enable);
         }
 
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-        public void setToggleSensorPrivacyState(int userId, int source, int sensor, int state) {
-            if (DEBUG) {
-                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
-                        + " callingPid=" + Binder.getCallingPid()
-                        + " setToggleSensorPrivacyState("
-                        + "userId=" + userId
-                        + " source=" + source
-                        + " sensor=" + sensor
-                        + " state=" + state
-                        + ")");
-            }
-            enforceManageSensorPrivacyPermission();
-            if (userId == UserHandle.USER_CURRENT) {
-                userId = mCurrentUser;
-            }
-
-            if (!canChangeToggleSensorPrivacy(userId, sensor)) {
-                return;
-            }
-            if (!supportsSensorToggle(TOGGLE_TYPE_SOFTWARE, sensor)) {
-                // Do not enable sensor privacy if the device doesn't support it.
-                return;
-            }
-
-            setToggleSensorPrivacyStateUnchecked(TOGGLE_TYPE_SOFTWARE, userId, source, sensor,
-                    state);
-        }
-
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        private void setToggleSensorPrivacyStateUnchecked(int toggleType, int userId, int source,
-                int sensor, int state) {
-            if (DEBUG) {
-                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
-                        + " callingPid=" + Binder.getCallingPid()
-                        + " setToggleSensorPrivacyStateUnchecked("
-                        + "userId=" + userId
-                        + " source=" + source
-                        + " sensor=" + sensor
-                        + " state=" + state
-                        + ")");
-            }
-            long[] lastChange = new long[1];
-            mSensorPrivacyStateController.atomic(() -> {
-                SensorState sensorState = mSensorPrivacyStateController
-                        .getState(toggleType, userId, sensor);
-                lastChange[0] = sensorState.getLastChange();
-                mSensorPrivacyStateController.setState(
-                        toggleType, userId, sensor, state, mHandler,
-                        changeSuccessful -> {
-                            if (changeSuccessful) {
-                                if (userId == mUserManagerInternal.getProfileParentId(userId)) {
-                                    mHandler.sendMessage(PooledLambda.obtainMessage(
-                                            SensorPrivacyServiceImpl::logSensorPrivacyStateToggle,
-                                            this,
-                                            source, sensor, state, lastChange[0], false));
-                                }
-                            }
-                        });
-            });
-        }
-
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        private void logSensorPrivacyStateToggle(int source, int sensor, int state,
-                long lastChange, boolean onShutDown) {
-            long logMins = Math.max(0, (getCurrentTimeMillis() - lastChange) / (1000 * 60));
-
-            int logAction = ACTION__ACTION_UNKNOWN;
-            if (!onShutDown) {
-                switch(state) {
-                    case ENABLED :
-                        logAction = ACTION__TOGGLE_OFF;
-                        break;
-                    case DISABLED :
-                        logAction = ACTION__TOGGLE_ON;
-                        break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS;
-                        break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS;
-                        break;
-                    case AUTOMOTIVE_DRIVER_ASSISTANCE_APPS :
-                        logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS;
-                        break;
-                    default :
-                        logAction = ACTION__ACTION_UNKNOWN;
-                        break;
-                }
-            }
-
-            int logSensor = PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__SENSOR_UNKNOWN;
-            switch(sensor) {
-                case CAMERA:
-                    logSensor = PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__CAMERA;
-                    break;
-                case MICROPHONE:
-                    logSensor = PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__MICROPHONE;
-                    break;
-                default:
-                    logSensor = PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__SENSOR_UNKNOWN;
-                    break;
-            }
-
-            int logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__SOURCE_UNKNOWN;
-            switch(source) {
-                case QS_TILE :
-                    logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__QS_TILE;
-                    break;
-                case DIALOG :
-                    logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__DIALOG;
-                    break;
-                case SETTINGS:
-                    logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__SETTINGS;
-                    break;
-                default:
-                    logSource = PRIVACY_SENSOR_TOGGLE_INTERACTION__SOURCE__SOURCE_UNKNOWN;
-                    break;
-            }
-
-            if (DEBUG || DEBUG_LOGGING) {
-                Log.d(TAG, "Logging sensor toggle interaction:" + " logSensor=" + logSensor
-                        + " logAction=" + logAction + " logSource=" + logSource + " logMins="
-                        + logMins);
-            }
-            write(PRIVACY_SENSOR_TOGGLE_INTERACTION, logSensor, logAction, logSource, logMins);
-
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
-        public void setToggleSensorPrivacyStateForProfileGroup(int userId, int source, int sensor,
-                int  state) {
-            enforceManageSensorPrivacyPermission();
-            if (userId == UserHandle.USER_CURRENT) {
-                userId = mCurrentUser;
-            }
-            int parentId = mUserManagerInternal.getProfileParentId(userId);
-            forAllUsers(userId2 -> {
-                if (parentId == mUserManagerInternal.getProfileParentId(userId2)) {
-                    setToggleSensorPrivacyState(userId2, source, sensor, state);
-                }
-            });
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-        public List<CameraPrivacyAllowlistEntry> getCameraPrivacyAllowlist() {
-            enforceObserveSensorPrivacyPermission();
-            return mCameraPrivacyAllowlist;
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-        public boolean isCameraPrivacyEnabled(String packageName) {
-            if (DEBUG) {
-                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
-                        + " callingPid=" + Binder.getCallingPid()
-                        + " isCameraPrivacyEnabled("
-                        + "packageName=" + packageName
-                        + ")");
-            }
-            enforceObserveSensorPrivacyPermission();
-
-            int state =  mSensorPrivacyStateController.getState(TOGGLE_TYPE_SOFTWARE, mCurrentUser,
-                    CAMERA).getState();
-            if (state == ENABLED) {
-                return true;
-            } else if (state == DISABLED) {
-                return false;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if ((packageName.equals(entry.packageName)) && !entry.isMandatory) {
-                        return false;
-                    }
-                }
-                return true;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if ((packageName.equals(entry.packageName)) && entry.isMandatory) {
-                        return false;
-                    }
-                }
-                return true;
-            } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_APPS) {
-                for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) {
-                    if (packageName.equals(entry.packageName)) {
-                        return false;
-                    }
-                }
-                return true;
-            }
-            return false;
-        }
-
-        @Override
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
-        public int getToggleSensorPrivacyState(int toggleType, int sensor) {
-            if (DEBUG) {
-                Log.d(TAG, "callingUid=" + Binder.getCallingUid()
-                        + " callingPid=" + Binder.getCallingPid()
-                        + " getToggleSensorPrivacyState("
-                        + "toggleType=" + toggleType
-                        + " sensor=" + sensor
-                        + ")");
-            }
-            enforceObserveSensorPrivacyPermission();
-
-            return mSensorPrivacyStateController.getState(toggleType, mCurrentUser, sensor)
-                    .getState();
-        }
-
         private void setToggleSensorPrivacyUnchecked(int toggleType, int userId, int source,
                 int sensor, boolean enable) {
             if (DEBUG) {
@@ -1181,23 +899,16 @@
          * Enforces the caller contains the necessary permission to change the state of sensor
          * privacy.
          */
-        @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
         private void enforceManageSensorPrivacyPermission() {
-            if (mContext.checkCallingOrSelfPermission(
-                    android.Manifest.permission.MANAGE_SENSOR_PRIVACY) == PERMISSION_GRANTED) {
-                return;
-            }
-
-            String message = "Changing sensor privacy requires the following permission: "
-                    + MANAGE_SENSOR_PRIVACY;
-            throw new SecurityException(message);
+            enforcePermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY,
+                    "Changing sensor privacy requires the following permission: "
+                            + MANAGE_SENSOR_PRIVACY);
         }
 
         /**
          * Enforces the caller contains the necessary permission to observe changes to the sate of
          * sensor privacy.
          */
-        @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY)
         private void enforceObserveSensorPrivacyPermission() {
             String systemUIPackage = mContext.getString(R.string.config_systemUi);
             int systemUIAppId = UserHandle.getAppId(mPackageManagerInternal
@@ -1206,13 +917,15 @@
                 // b/221782106, possible race condition with role grant might bootloop device.
                 return;
             }
-            if (mContext.checkCallingOrSelfPermission(
-                    android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) == PERMISSION_GRANTED) {
+            enforcePermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY,
+                    "Observing sensor privacy changes requires the following permission: "
+                            + android.Manifest.permission.OBSERVE_SENSOR_PRIVACY);
+        }
+
+        private void enforcePermission(String permission, String message) {
+            if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
                 return;
             }
-
-            String message = "Observing sensor privacy changes requires the following permission: "
-                    + android.Manifest.permission.OBSERVE_SENSOR_PRIVACY;
             throw new SecurityException(message);
         }
 
@@ -1580,13 +1293,11 @@
         }
 
         @Override
-        @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
         public void onShellCommand(FileDescriptor in, FileDescriptor out,
                 FileDescriptor err, String[] args, ShellCallback callback,
                 ResultReceiver resultReceiver) {
             (new ShellCommand() {
                 @Override
-                @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY)
                 public int onCommand(String cmd) {
                     if (cmd == null) {
                         return handleDefaultCommands(cmd);
@@ -1616,45 +1327,6 @@
                             setToggleSensorPrivacy(userId, SHELL, sensor, false);
                         }
                         break;
-                        case "automotive_driver_assistance_apps" : {
-                            if (Flags.privacyAllowlist()) {
-                                int sensor = sensorStrToId(getNextArgRequired());
-                                if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
-                                    pw.println("Command not valid for this sensor");
-                                    return -1;
-                                }
-
-                                setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_APPS);
-                            }
-                        }
-                        break;
-                        case "automotive_driver_assistance_helpful_apps" : {
-                            if (Flags.privacyAllowlist()) {
-                                int sensor = sensorStrToId(getNextArgRequired());
-                                if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
-                                    pw.println("Command not valid for this sensor");
-                                    return -1;
-                                }
-
-                                setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS);
-                            }
-                        }
-                        break;
-                        case "automotive_driver_assistance_required_apps" : {
-                            if (Flags.privacyAllowlist()) {
-                                int sensor = sensorStrToId(getNextArgRequired());
-                                if ((!isAutomotive(mContext)) || (sensor != CAMERA)) {
-                                    pw.println("Command not valid for this sensor");
-                                    return -1;
-                                }
-
-                                setToggleSensorPrivacyState(userId, SHELL, sensor,
-                                        AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS);
-                            }
-                        }
-                        break;
                         default:
                             return handleDefaultCommands(cmd);
                     }
@@ -1677,24 +1349,6 @@
                     pw.println("  disable USER_ID SENSOR");
                     pw.println("    Disable privacy for a certain sensor.");
                     pw.println("");
-                    if (Flags.privacyAllowlist()) {
-                        if (isAutomotive(mContext)) {
-                            pw.println("  automotive_driver_assistance_apps USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which help you"
-                                    + " drive and apps which are required by OEM");
-                            pw.println("");
-                            pw.println("  automotive_driver_assistance_helpful_apps "
-                                    + "USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which "
-                                    + "help you drive.");
-                            pw.println("");
-                            pw.println("  automotive_driver_assistance_required_apps "
-                                    + "USER_ID SENSOR");
-                            pw.println("    Disable privacy for automotive apps which are "
-                                    + "required by OEM.");
-                            pw.println("");
-                        }
-                    }
                 }
             }).exec(this, in, out, err, args, callback, resultReceiver);
         }
@@ -1803,38 +1457,6 @@
             mSensorPrivacyServiceImpl.showSensorStateChangedActivity(sensor, toggleType);
         }
 
-        @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-        public void handleSensorPrivacyChanged(int userId, int toggleType, int sensor,
-                int state) {
-            if (userId == mCurrentUser) {
-                mSensorPrivacyServiceImpl.setGlobalRestriction(sensor,
-                        mSensorPrivacyServiceImpl.isCombinedToggleSensorPrivacyEnabled(sensor));
-            }
-
-            if (userId != mCurrentUser) {
-                return;
-            }
-            synchronized (mListenerLock) {
-                try {
-                    final int count = mToggleSensorListeners.beginBroadcast();
-                    for (int i = 0; i < count; i++) {
-                        ISensorPrivacyListener listener = mToggleSensorListeners.getBroadcastItem(
-                                i);
-                        try {
-                            listener.onSensorPrivacyStateChanged(toggleType, sensor, state);
-                        } catch (RemoteException e) {
-                            Log.e(TAG, "Caught an exception notifying listener " + listener + ": ",
-                                    e);
-                        }
-                    }
-                } finally {
-                    mToggleSensorListeners.finishBroadcast();
-                }
-            }
-
-            mSensorPrivacyServiceImpl.showSensorStateChangedActivity(sensor, toggleType);
-        }
-
         public void removeSuppressPackageReminderToken(Pair<Integer, UserHandle> key,
                 IBinder token) {
             sendMessage(PooledLambda.obtainMessage(
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java
index 0e29222..9694958 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateController.java
@@ -16,11 +16,9 @@
 
 package com.android.server.sensorprivacy;
 
-import android.annotation.FlaggedApi;
 import android.os.Handler;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.pooled.PooledLambda;
 
@@ -53,14 +51,6 @@
         }
     }
 
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    void setState(int toggleType, int userId, int sensor, int state, Handler callbackHandler,
-            SetStateResultCallback callback) {
-        synchronized (mLock) {
-            setStateLocked(toggleType, userId, sensor, state, callbackHandler, callback);
-        }
-    }
-
     void setSensorPrivacyListener(Handler handler,
             SensorPrivacyListener listener) {
         synchronized (mLock) {
@@ -138,11 +128,6 @@
             Handler callbackHandler, SetStateResultCallback callback);
 
     @GuardedBy("mLock")
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    abstract void setStateLocked(int toggleType, int userId, int sensor, int state,
-            Handler callbackHandler, SetStateResultCallback callback);
-
-    @GuardedBy("mLock")
     abstract void setSensorPrivacyListenerLocked(Handler handler,
             SensorPrivacyListener listener);
 
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java
index 2d96aeb..3dcb4cf 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyStateControllerImpl.java
@@ -16,12 +16,8 @@
 
 package com.android.server.sensorprivacy;
 
-import static android.hardware.SensorPrivacyManager.StateTypes.DISABLED;
-
-import android.annotation.FlaggedApi;
 import android.os.Handler;
 
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.pooled.PooledLambda;
 
@@ -89,33 +85,6 @@
         sendSetStateCallback(callbackHandler, callback, false);
     }
 
-    @Override
-    @FlaggedApi(Flags.FLAG_PRIVACY_ALLOWLIST)
-    void setStateLocked(int toggleType, int userId, int sensor, int state,
-            Handler callbackHandler, SetStateResultCallback callback) {
-        // Changing the SensorState's mEnabled updates the timestamp of its last change.
-        // A nonexistent state -> unmuted should not set the timestamp.
-        SensorState lastState = mPersistedState.getState(toggleType, userId, sensor);
-        if (lastState == null) {
-            if (state == DISABLED) {
-                sendSetStateCallback(callbackHandler, callback, false);
-                return;
-            } else {
-                SensorState sensorState = new SensorState(state);
-                mPersistedState.setState(toggleType, userId, sensor, sensorState);
-                notifyStateChangeLocked(toggleType, userId, sensor, sensorState);
-                sendSetStateCallback(callbackHandler, callback, true);
-                return;
-            }
-        }
-        if (lastState.setState(state)) {
-            notifyStateChangeLocked(toggleType, userId, sensor, lastState);
-            sendSetStateCallback(callbackHandler, callback, true);
-            return;
-        }
-        sendSetStateCallback(callbackHandler, callback, false);
-    }
-
     private void notifyStateChangeLocked(int toggleType, int userId, int sensor,
             SensorState sensorState) {
         if (mListenerHandler != null && mListener != null) {
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index dfa9dce..3607ddd 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -34,6 +34,7 @@
         "tvinput/BufferProducerThread.cpp",
         "tvinput/JTvInputHal.cpp",
         "tvinput/TvInputHal_hidl.cpp",
+        "com_android_server_accessibility_BrailleDisplayConnection.cpp",
         "com_android_server_adb_AdbDebuggingManager.cpp",
         "com_android_server_am_BatteryStatsService.cpp",
         "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp",
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index df7fb99..b999305f 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -15,6 +15,7 @@
 per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS
 per-file com_android_server_Usb* = file:/services/usb/OWNERS
 per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file com_android_server_accessibility_* = file:/services/accessibility/OWNERS
 per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
 per-file com_android_server_lights_* = file:/services/core/java/com/android/server/lights/OWNERS
 per-file com_android_server_location_* = file:/location/java/android/location/OWNERS
diff --git a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
new file mode 100644
index 0000000..9a509a7
--- /dev/null
+++ b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <core_jni_helpers.h>
+#include <jni.h>
+#include <linux/hidraw.h>
+#include <linux/input.h>
+#include <nativehelper/JNIHelp.h>
+#include <sys/ioctl.h>
+
+/*
+ * This file defines simple wrappers around the kernel UAPI HIDRAW driver's ioctl() commands.
+ * See kernel example samples/hidraw/hid-example.c
+ *
+ * All methods expect an open file descriptor int from Java.
+ */
+
+namespace android {
+
+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;
+
+} // anonymous namespace
+
+static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize(
+        JNIEnv* env, jobject thiz, int fd) {
+    int size = 0;
+    if (ioctl(fd, HIDIOCGRDESCSIZE, &size) < 0) {
+        return -1;
+    }
+    return size;
+}
+
+static jbyteArray com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc(
+        JNIEnv* env, jobject thiz, int fd, int descSize) {
+    struct hidraw_report_descriptor desc;
+    desc.size = descSize;
+    if (ioctl(fd, HIDIOCGRDESC, &desc) < 0) {
+        return nullptr;
+    }
+    jbyteArray result = env->NewByteArray(descSize);
+    if (result != nullptr) {
+        env->SetByteArrayRegion(result, 0, descSize, (jbyte*)desc.value);
+    }
+    // Local ref is not deleted because it is returned to Java
+    return result;
+}
+
+static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq(JNIEnv* env,
+                                                                                       jobject thiz,
+                                                                                       int fd) {
+    char buf[UNIQ_SIZE_MAX];
+    if (ioctl(fd, HIDIOCGRAWUNIQ(UNIQ_SIZE_MAX), buf) < 0) {
+        return nullptr;
+    }
+    // Local ref is not deleted because it is returned to Java
+    return env->NewStringUTF(buf);
+}
+
+static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType(JNIEnv* env,
+                                                                                       jobject thiz,
+                                                                                       int fd) {
+    struct hidraw_devinfo info;
+    if (ioctl(fd, HIDIOCGRAWINFO, &info) < 0) {
+        return -1;
+    }
+    return info.bustype;
+}
+
+static const JNINativeMethod gMethods[] = {
+        {"nativeGetHidrawDescSize", "(I)I",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize},
+        {"nativeGetHidrawDesc", "(II)[B",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc},
+        {"nativeGetHidrawUniq", "(I)Ljava/lang/String;",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq},
+        {"nativeGetHidrawBusType", "(I)I",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType},
+};
+
+int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env) {
+    return RegisterMethodsOrDie(env, "com/android/server/accessibility/BrailleDisplayConnection",
+                                gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 5d1eb49..0936888 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -70,6 +70,7 @@
 int register_com_android_server_display_DisplayControl(JNIEnv* env);
 int register_com_android_server_SystemClockTime(JNIEnv* env);
 int register_android_server_display_smallAreaDetectionController(JNIEnv* env);
+int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env);
 };
 
 using namespace android;
@@ -132,5 +133,6 @@
     register_com_android_server_display_DisplayControl(env);
     register_com_android_server_SystemClockTime(env);
     register_android_server_display_smallAreaDetectionController(env);
+    register_com_android_server_accessibility_BrailleDisplayConnection(env);
     return JNI_VERSION_1_4;
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 05d1c49..85eac29 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -98,6 +98,7 @@
 import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS;
 import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_SUSPENSION;
 import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
+import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
 import static android.app.admin.DeviceAdminInfo.USES_POLICY_FORCE_LOCK;
 import static android.app.admin.DeviceAdminInfo.USES_POLICY_WIPE_DATA;
 import static android.app.admin.DeviceAdminReceiver.ACTION_COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED;
@@ -189,6 +190,7 @@
 import static android.app.admin.DevicePolicyManager.STATUS_MANAGED_USERS_NOT_SUPPORTED;
 import static android.app.admin.DevicePolicyManager.STATUS_NONSYSTEM_USER_EXISTS;
 import static android.app.admin.DevicePolicyManager.STATUS_NOT_SYSTEM_USER;
+import static android.app.admin.DevicePolicyManager.STATUS_HEADLESS_ONLY_SYSTEM_USER;
 import static android.app.admin.DevicePolicyManager.STATUS_OK;
 import static android.app.admin.DevicePolicyManager.STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS;
 import static android.app.admin.DevicePolicyManager.STATUS_SYSTEM_USER;
@@ -225,6 +227,7 @@
 import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED;
 import static android.app.admin.flags.Flags.backupServiceSecurityLogEventEnabled;
 import static android.app.admin.flags.Flags.dumpsysPolicyEngineMigrationEnabled;
+import static android.app.admin.flags.Flags.headlessDeviceOwnerSingleUserEnabled;
 import static android.app.admin.flags.Flags.policyEngineMigrationV2Enabled;
 import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE;
 import static android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE;
@@ -9460,7 +9463,8 @@
         }
 
         if (setProfileOwnerOnCurrentUserIfNecessary
-                && mInjector.userManagerIsHeadlessSystemUserMode()) {
+                && mInjector.userManagerIsHeadlessSystemUserMode()
+                && getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED) {
             int currentForegroundUser;
             synchronized (getLockObject()) {
                 currentForegroundUser = getCurrentForegroundUserId();
@@ -9476,6 +9480,12 @@
         return true;
     }
 
+    private int getHeadlessDeviceOwnerMode() {
+        synchronized (getLockObject()) {
+            return getDeviceOwnerAdminLocked().info.getHeadlessDeviceOwnerMode();
+        }
+    }
+
     /**
      * This API is cached: invalidate with invalidateBinderCaches().
      */
@@ -12226,6 +12236,13 @@
                     + admin + " are not in the same package");
         }
         final CallerIdentity caller = getCallerIdentity(admin);
+
+        if (headlessDeviceOwnerSingleUserEnabled()) {
+            // Block this method if the device is in headless main user mode
+            Preconditions.checkCallAuthorization(
+                    getHeadlessDeviceOwnerMode() != HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER,
+                    "createAndManageUser was called while in headless single user mode");
+        }
         // Only allow the system user to use this method
         Preconditions.checkCallAuthorization(caller.getUserHandle().isSystem(),
                 "createAndManageUser was called from non-system user");
@@ -16636,29 +16653,51 @@
             return STATUS_USER_NOT_RUNNING;
         }
 
+        DeviceAdminInfo adminInfo = null;
+
+        boolean isHeadlessModeAffiliated = false;
+
+        boolean isHeadlessModeSingleUser = false;
+
         boolean isHeadlessSystemUserMode = mInjector.userManagerIsHeadlessSystemUserMode();
 
+        int ensureSetUpUser = UserHandle.USER_SYSTEM;
         if (isHeadlessSystemUserMode) {
-            if (deviceOwnerUserId != UserHandle.USER_SYSTEM) {
+            if (owner != null) {
+                adminInfo = findAdmin(owner,
+                        deviceOwnerUserId, /* throwForMissingPermission= */ false);
+
+                isHeadlessModeAffiliated =
+                        adminInfo.getHeadlessDeviceOwnerMode()
+                                == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
+
+                isHeadlessModeSingleUser =
+                        adminInfo.getHeadlessDeviceOwnerMode()
+                                == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
+
+                if (!isHeadlessModeAffiliated && !isHeadlessModeSingleUser) {
+                    return STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED;
+                }
+
+                if (headlessDeviceOwnerSingleUserEnabled() && isHeadlessModeSingleUser) {
+                    ensureSetUpUser = mUserManagerInternal.getMainUserId();
+                    if (ensureSetUpUser == UserHandle.USER_NULL) {
+                        return STATUS_HEADLESS_ONLY_SYSTEM_USER;
+                    }
+                }
+            }
+
+            if (isHeadlessModeAffiliated && deviceOwnerUserId != UserHandle.USER_SYSTEM) {
                 Slogf.e(LOG_TAG, "In headless system user mode, "
                         + "device owner can only be set on headless system user.");
                 return STATUS_NOT_SYSTEM_USER;
             }
 
-            if (owner != null) {
-                DeviceAdminInfo adminInfo = findAdmin(
-                        owner, deviceOwnerUserId, /* throwForMissingPermission= */ false);
-
-                if (adminInfo.getHeadlessDeviceOwnerMode()
-                        != HEADLESS_DEVICE_OWNER_MODE_AFFILIATED) {
-                    return STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED;
-                }
-            }
         }
 
         if (isAdb) {
             // If shell command runs after user setup completed check device status. Otherwise, OK.
-            if (hasUserSetupCompleted(UserHandle.USER_SYSTEM)) {
+            if (hasUserSetupCompleted(ensureSetUpUser)) {
                 // DO can be setup only if there are no users which are neither created by default
                 // nor marked as FOR_TESTING
 
@@ -16681,11 +16720,12 @@
             return STATUS_OK;
         } else {
             // DO has to be user 0
-            if (deviceOwnerUserId != UserHandle.USER_SYSTEM) {
+            if ((!isHeadlessSystemUserMode || isHeadlessModeAffiliated)
+                    && deviceOwnerUserId != UserHandle.USER_SYSTEM) {
                 return STATUS_NOT_SYSTEM_USER;
             }
             // Only provision DO before setup wizard completes
-            if (hasUserSetupCompleted(UserHandle.USER_SYSTEM)) {
+            if (hasUserSetupCompleted(ensureSetUpUser)) {
                 return STATUS_USER_SETUP_COMPLETED;
             }
             return STATUS_OK;
@@ -21260,7 +21300,11 @@
             setTimeAndTimezone(provisioningParams.getTimeZone(), provisioningParams.getLocalTime());
             setLocale(provisioningParams.getLocale());
 
-            int deviceOwnerUserId = UserHandle.USER_SYSTEM;
+            int deviceOwnerUserId = headlessDeviceOwnerSingleUserEnabled()
+                    && getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER
+                    ? mUserManagerInternal.getMainUserId()
+                    : UserHandle.USER_SYSTEM;
+
             if (!removeNonRequiredAppsForManagedDevice(
                     deviceOwnerUserId,
                     provisioningParams.isLeaveAllSystemAppsEnabled(),
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
index a8100af..66e0717 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
@@ -18,6 +18,10 @@
 
 import android.content.Context
 import android.content.Intent
+import android.content.UriRelativeFilter
+import android.content.UriRelativeFilterGroup
+import android.content.UriRelativeFilterGroupParcel
+import android.content.pm.Flags
 import android.content.pm.PackageManager
 import android.content.pm.verify.domain.DomainOwner
 import android.content.pm.verify.domain.DomainVerificationInfo
@@ -25,8 +29,10 @@
 import android.content.pm.verify.domain.DomainVerificationUserState
 import android.content.pm.verify.domain.IDomainVerificationManager
 import android.os.Build
-import android.os.PatternMatcher
+import android.os.Bundle
+import android.os.PatternMatcher.PATTERN_LITERAL
 import android.os.Process
+import android.platform.test.annotations.RequiresFlagsEnabled
 import android.util.ArraySet
 import android.util.SparseArray
 import com.android.internal.pm.parsing.pkg.AndroidPackageInternal
@@ -68,6 +74,63 @@
         private val DOMAIN_4 = "four.$DOMAIN_BASE"
     }
 
+    @RequiresFlagsEnabled(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+    @Test
+    fun updateUriRelativeFilterGroups() {
+        val pkgWithDomains = mockPkgState(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2))
+        val service = makeService(pkgWithDomains).apply {
+            addPackages(pkgWithDomains)
+        }
+
+        val bundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1, DOMAIN_2))
+        assertThat(bundle.keySet()).containsExactlyElementsIn(listOf(DOMAIN_1, DOMAIN_2))
+        assertThat(bundle.getParcelableArrayList(DOMAIN_1, UriRelativeFilterGroup::class.java))
+            .isEmpty()
+        assertThat(bundle.getParcelableArrayList(DOMAIN_2, UriRelativeFilterGroup::class.java))
+            .isEmpty()
+
+        val pathGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW)
+        pathGroup.addUriRelativeFilter(
+            UriRelativeFilter(UriRelativeFilter.PATH, PATTERN_LITERAL, "path")
+        )
+        val queryGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_BLOCK)
+        queryGroup.addUriRelativeFilter(
+            UriRelativeFilter(UriRelativeFilter.QUERY, PATTERN_LITERAL, "query")
+        )
+        val fragmentGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW)
+        fragmentGroup.addUriRelativeFilter(
+            UriRelativeFilter(UriRelativeFilter.FRAGMENT, PATTERN_LITERAL, "fragment")
+        )
+
+        assertGroups(service, arrayListOf(pathGroup))
+        assertGroups(service, arrayListOf(queryGroup, pathGroup))
+        assertGroups(service, arrayListOf(queryGroup, fragmentGroup, pathGroup))
+    }
+
+    private fun assertGroups(
+        service: DomainVerificationService,
+        groups: List<UriRelativeFilterGroup>
+    ) {
+        val bundle = Bundle()
+        bundle.putParcelableList(DOMAIN_1, UriRelativeFilterGroup.groupsToParcels(groups))
+        service.setUriRelativeFilterGroups(PKG_ONE, bundle)
+        val fetchedBundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1))
+        assertThat(fetchedBundle.keySet()).containsExactlyElementsIn(bundle.keySet())
+        assertThat(
+            UriRelativeFilterGroup.parcelsToGroups(
+                fetchedBundle.getParcelableArrayList(
+                    DOMAIN_1,
+                    UriRelativeFilterGroupParcel::class.java)
+            )
+        ).containsExactlyElementsIn(
+            UriRelativeFilterGroup.parcelsToGroups(
+                bundle.getParcelableArrayList(
+                    DOMAIN_1,
+                    UriRelativeFilterGroupParcel::class.java)
+            )
+        ).inOrder()
+    }
+
     @Test
     fun queryValidVerificationPackageNames() {
         val pkgWithDomains = mockPkgState(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2))
@@ -484,6 +547,7 @@
         DomainVerificationService(mockThrowOnUnmocked {
             // Assume the test has every permission necessary
             whenever(enforcePermission(anyString(), anyInt(), anyInt(), anyString()))
+            whenever(enforceCallingOrSelfPermission(anyString(), anyString()))
             whenever(checkPermission(anyString(), anyInt(), anyInt())) {
                 PackageManager.PERMISSION_GRANTED
             }
@@ -539,7 +603,7 @@
                                     addCategory(Intent.CATEGORY_DEFAULT)
                                     addDataScheme("http")
                                     addDataScheme("https")
-                                    addDataPath("/sub", PatternMatcher.PATTERN_LITERAL)
+                                    addDataPath("/sub", PATTERN_LITERAL)
                                     addDataAuthority(it, null)
                                 }
                             }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
index 65b99c5..4fa4190 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
@@ -16,7 +16,12 @@
 
 package com.android.server.pm.test.verify.domain
 
+import android.content.UriRelativeFilter
+import android.content.UriRelativeFilter.PATH
+import android.content.UriRelativeFilterGroup
+import android.content.UriRelativeFilterGroup.ACTION_ALLOW
 import android.content.pm.verify.domain.DomainVerificationState
+import android.os.PatternMatcher.PATTERN_LITERAL
 import android.os.UserHandle
 import android.util.ArrayMap
 import android.util.SparseArray
@@ -157,7 +162,7 @@
 
     @Test
     fun writeStateSignatureIfFunctionReturnsNull() {
-        val (attached, pending, restored) = mockWriteValues  { "SIGNATURE_$it" }
+        val (attached, pending, restored) = mockWriteValues { "SIGNATURE_$it" }
         val file = tempFolder.newFile().writeXml {
             DomainVerificationPersistence.writeToXml(it, attached, pending, restored,
                     UserHandle.USER_ALL) { null }
@@ -313,6 +318,9 @@
                     addHosts(setOf("$packageName-user.com"))
                     isLinkHandlingAllowed = true
                 }
+                val group = UriRelativeFilterGroup(ACTION_ALLOW)
+                group.addUriRelativeFilter(UriRelativeFilter(PATH, PATTERN_LITERAL, "test"))
+                uriRelativeFilterGroupMap.put("example.com", listOf(group))
             }
 
     private fun pkgName(id: Int) = "$PKG_PREFIX.pkg$id"
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 28471b3..6bcd778 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -49,7 +49,6 @@
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONTENT_TRIGGER;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE;
-import static com.android.server.job.controllers.JobStatus.MIN_WINDOW_FOR_FLEXIBILITY_MS;
 import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -410,10 +409,12 @@
 
     @Test
     public void testOnConstantsUpdated_PercentsToDropConstraints() {
+        final long fallbackDuration = 12 * HOUR_IN_MILLIS;
         JobInfo.Builder jb = createJob(0)
-                .setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+                .setOverrideDeadline(HOUR_IN_MILLIS);
         JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+        // Even though the override deadline is 1 hour, the fallback duration is still used.
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 5,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
         setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
                 "500=1|2|3|4"
@@ -441,13 +442,13 @@
                 mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
                         .get(JobInfo.PRIORITY_MIN),
                 new int[]{54, 55, 56, 57});
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
         js.setNumDroppedFlexibleConstraints(1);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 2,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 2,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
         js.setNumDroppedFlexibleConstraints(2);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 3,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 3,
                 mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
     }
 
@@ -504,37 +505,38 @@
 
     @Test
     public void testGetNextConstraintDropTimeElapsedLocked() {
+        final long fallbackDuration = 50 * HOUR_IN_MILLIS;
         setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 200 * HOUR_IN_MILLIS);
         setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES,
                 "500=" + HOUR_IN_MILLIS
                         + ",400=" + 25 * HOUR_IN_MILLIS
-                        + ",300=" + 50 * HOUR_IN_MILLIS
+                        + ",300=" + fallbackDuration
                         + ",200=" + 100 * HOUR_IN_MILLIS
                         + ",100=" + 200 * HOUR_IN_MILLIS);
 
         long nextTimeToDropNumConstraints;
 
         // no delay, deadline
-        JobInfo.Builder jb = createJob(0).setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+        JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
         JobStatus js = createJobStatus("time", jb);
 
         assertEquals(JobStatus.NO_EARLIEST_RUNTIME, js.getEarliestRunTime());
-        assertEquals(MIN_WINDOW_FOR_FLEXIBILITY_MS + FROZEN_TIME, js.getLatestRunTimeElapsed());
+        assertEquals(HOUR_IN_MILLIS + FROZEN_TIME, js.getLatestRunTimeElapsed());
         assertEquals(FROZEN_TIME, js.enqueueTime);
 
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 5,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(1);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 6,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(2);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7,
+        assertEquals(FROZEN_TIME + fallbackDuration / 10 * 7,
                 nextTimeToDropNumConstraints);
 
         // delay, no deadline
@@ -574,81 +576,83 @@
 
         // delay, deadline
         jb = createJob(0)
-                .setOverrideDeadline(2 * MIN_WINDOW_FOR_FLEXIBILITY_MS)
-                .setMinimumLatency(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+                .setOverrideDeadline(2 * HOUR_IN_MILLIS)
+                .setMinimumLatency(HOUR_IN_MILLIS);
         js = createJobStatus("time", jb);
 
-        final long windowStart = FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS;
+        final long windowStart = FROZEN_TIME + HOUR_IN_MILLIS;
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+        assertEquals(windowStart + fallbackDuration / 10 * 5,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(1);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6,
+        assertEquals(windowStart + fallbackDuration / 10 * 6,
                 nextTimeToDropNumConstraints);
         js.setNumDroppedFlexibleConstraints(2);
         nextTimeToDropNumConstraints = mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js);
-        assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7,
+        assertEquals(windowStart + fallbackDuration / 10 * 7,
                 nextTimeToDropNumConstraints);
     }
 
     @Test
     public void testCurPercent() {
+        final long fallbackDuration = 10 * HOUR_IN_MILLIS;
+        setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, "300=" + fallbackDuration);
         long deadline = 100 * MINUTE_IN_MILLIS;
         long nowElapsed = FROZEN_TIME;
         JobInfo.Builder jb = createJob(0).setOverrideDeadline(deadline);
         JobStatus js = createJobStatus("time", jb);
 
         assertEquals(FROZEN_TIME, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
-        assertEquals(deadline + FROZEN_TIME,
+        assertEquals(FROZEN_TIME + fallbackDuration,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, FROZEN_TIME));
-        nowElapsed = FROZEN_TIME + 60 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + 6 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
         assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
-        nowElapsed = FROZEN_TIME + 130 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + 13 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
         assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
-        nowElapsed = FROZEN_TIME + 95 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + 9 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
-        assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
+        assertEquals(90, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
         nowElapsed = FROZEN_TIME;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
-        long delay = MINUTE_IN_MILLIS;
-        deadline = 101 * MINUTE_IN_MILLIS;
+        long delay = HOUR_IN_MILLIS;
+        deadline = HOUR_IN_MILLIS + 100 * MINUTE_IN_MILLIS;
         jb = createJob(0).setOverrideDeadline(deadline).setMinimumLatency(delay);
         js = createJobStatus("time", jb);
 
         assertEquals(FROZEN_TIME + delay,
                 mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
-        assertEquals(deadline + FROZEN_TIME,
+        assertEquals(FROZEN_TIME + delay + fallbackDuration,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed,
                         FROZEN_TIME + delay));
 
-        nowElapsed = FROZEN_TIME + delay + 60 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + delay + 6 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
         assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
-        nowElapsed = FROZEN_TIME + 130 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + 13 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
         assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
-        nowElapsed = FROZEN_TIME + delay + 95 * MINUTE_IN_MILLIS;
+        nowElapsed = FROZEN_TIME + delay + 9 * HOUR_IN_MILLIS;
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
-        assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
+        assertEquals(90, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
     }
 
     @Test
@@ -786,26 +790,27 @@
         // deadline
         JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
         JobStatus js = createJobStatus("time", jb);
-        assertEquals(HOUR_IN_MILLIS + FROZEN_TIME,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+        assertEquals(3 * HOUR_IN_MILLIS + js.enqueueTime,
+                mFlexibilityController
+                        .getLifeCycleEndElapsedLocked(js, nowElapsed, js.enqueueTime));
 
         // no deadline
-        assertEquals(FROZEN_TIME + 2 * HOUR_IN_MILLIS,
+        assertEquals(js.enqueueTime + 2 * HOUR_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_HIGH)),
-                        nowElapsed, 100L));
-        assertEquals(FROZEN_TIME + 3 * HOUR_IN_MILLIS,
+                        nowElapsed, js.enqueueTime));
+        assertEquals(js.enqueueTime + 3 * HOUR_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)),
-                        nowElapsed, 100L));
-        assertEquals(FROZEN_TIME + 4 * HOUR_IN_MILLIS,
+                        nowElapsed, js.enqueueTime));
+        assertEquals(js.enqueueTime + 4 * HOUR_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_LOW)),
-                        nowElapsed, 100L));
-        assertEquals(FROZEN_TIME + 5 * HOUR_IN_MILLIS,
+                        nowElapsed, js.enqueueTime));
+        assertEquals(js.enqueueTime + 5 * HOUR_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_MIN)),
-                        nowElapsed, 100L));
+                        nowElapsed, js.enqueueTime));
     }
 
     @Test
@@ -871,14 +876,16 @@
         mFlexibilityController.prepareForExecutionLocked(jsLow);
         mFlexibilityController.prepareForExecutionLocked(jsMin);
 
-        // deadline
-        JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
-        JobStatus js = createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", jb);
-        assertEquals(HOUR_IN_MILLIS + FROZEN_TIME,
-                mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+        final long longDeadlineMs = 14 * 24 * HOUR_IN_MILLIS;
+        JobInfo.Builder jbWithLongDeadline = createJob(0).setOverrideDeadline(longDeadlineMs);
+        JobStatus jsWithLongDeadline = createJobStatus(
+                "testGetLifeCycleEndElapsedLocked_ScoreAddition", jbWithLongDeadline);
+        JobInfo.Builder jbWithShortDeadline =
+                createJob(0).setOverrideDeadline(15 * MINUTE_IN_MILLIS);
+        JobStatus jsWithShortDeadline = createJobStatus(
+                "testGetLifeCycleEndElapsedLocked_ScoreAddition", jbWithShortDeadline);
 
         final long earliestMs = 123L;
-        // no deadline
         assertEquals(earliestMs + HOUR_IN_MILLIS + 5 * 15 * MINUTE_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
@@ -894,6 +901,9 @@
                         createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
                                 createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)),
                         nowElapsed, earliestMs));
+        assertEquals(earliestMs + HOUR_IN_MILLIS + 3 * 15 * MINUTE_IN_MILLIS,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        jsWithShortDeadline, nowElapsed, earliestMs));
         assertEquals(earliestMs + HOUR_IN_MILLIS + 2 * 15 * MINUTE_IN_MILLIS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(
                         createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
@@ -904,6 +914,9 @@
                         createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
                                 createJob(0).setPriority(JobInfo.PRIORITY_MIN)),
                         nowElapsed, earliestMs));
+        assertEquals(jsWithLongDeadline.enqueueTime + longDeadlineMs,
+                mFlexibilityController.getLifeCycleEndElapsedLocked(
+                        jsWithLongDeadline, nowElapsed, earliestMs));
     }
 
     @Test
@@ -1033,8 +1046,8 @@
         JobInfo.Builder jb = createJob(0);
         jb.setMinimumLatency(1);
         jb.setOverrideDeadline(2);
-        JobStatus js = createJobStatus("Disable Flexible When Job Has Short Window", jb);
-        assertFalse(js.hasFlexibilityConstraint());
+        JobStatus js = createJobStatus("testExceptions_ShortWindow", jb);
+        assertTrue(js.hasFlexibilityConstraint());
     }
 
     @Test
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index 64fef68..1de049e 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -8,6 +8,37 @@
     srcs: [
         "src/com/android/server/power/stats/AggregatedPowerStatsProcessorTest.java",
         "src/com/android/server/power/stats/AggregatedPowerStatsTest.java",
+        "src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/AudioPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/BatteryChargeCalculatorTest.java",
+        "src/com/android/server/power/stats/BatteryStatsCounterTest.java",
+        "src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java",
+        "src/com/android/server/power/stats/BatteryStatsDualTimerTest.java",
+        "src/com/android/server/power/stats/BatteryStatsDurationTimerTest.java",
+        "src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java",
+        "src/com/android/server/power/stats/BatteryStatsHistoryTest.java",
+        "src/com/android/server/power/stats/BatteryStatsImplTest.java",
+        "src/com/android/server/power/stats/BatteryStatsNoteTest.java",
+        "src/com/android/server/power/stats/BatteryStatsSamplingTimerTest.java",
+        "src/com/android/server/power/stats/BatteryStatsSensorTest.java",
+        "src/com/android/server/power/stats/BatteryStatsServTest.java",
+        "src/com/android/server/power/stats/BatteryStatsStopwatchTimerTest.java",
+        "src/com/android/server/power/stats/BatteryStatsTimeBaseTest.java",
+        "src/com/android/server/power/stats/BatteryStatsTimerTest.java",
+        "src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java",
+        "src/com/android/server/power/stats/BatteryUsageStatsTest.java",
+        "src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/CameraPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java",
+        "src/com/android/server/power/stats/CpuPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java",
+        "src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/GnssPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/IdlePowerCalculatorTest.java",
+        "src/com/android/server/power/stats/LongSamplingCounterArrayTest.java",
+        "src/com/android/server/power/stats/LongSamplingCounterTest.java",
+        "src/com/android/server/power/stats/MemoryPowerCalculatorTest.java",
         "src/com/android/server/power/stats/MultiStateStatsTest.java",
         "src/com/android/server/power/stats/PowerStatsAggregatorTest.java",
         "src/com/android/server/power/stats/PowerStatsCollectorTest.java",
@@ -15,6 +46,12 @@
         "src/com/android/server/power/stats/PowerStatsSchedulerTest.java",
         "src/com/android/server/power/stats/PowerStatsStoreTest.java",
         "src/com/android/server/power/stats/PowerStatsUidResolverTest.java",
+        "src/com/android/server/power/stats/ScreenPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/SensorPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/UserPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/VideoPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/WakelockPowerCalculatorTest.java",
+        "src/com/android/server/power/stats/WifiPowerCalculatorTest.java",
     ],
 }
 
@@ -26,10 +63,6 @@
         "src/**/*.java",
     ],
 
-    exclude_srcs: [
-        ":power_stats_ravenwood_tests",
-    ],
-
     static_libs: [
         "services.core",
         "coretests-aidl",
@@ -41,6 +74,7 @@
         "androidx.test.ext.truth",
         "androidx.test.uiautomator_uiautomator",
         "mockito-target-minus-junit4",
+        "ravenwood-junit",
         "servicestests-utils",
         "platform-test-annotations",
         "flag-junit",
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java
index 319a280..f74cfae 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java
@@ -21,6 +21,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.BatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.view.Display;
 
 import androidx.test.filters.SmallTest;
@@ -34,10 +35,15 @@
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class AmbientDisplayPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
     private static final long MINUTE_IN_MS = 60 * 1000;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 0, 10.0)
             .setNumDisplays(1);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java
index fb367b2..ce451c2 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java
@@ -21,6 +21,7 @@
 import android.os.BatteryConsumer;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -34,11 +35,16 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class AudioPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_AUDIO, 360.0);
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
index 3f058a2..3ab1c2e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
@@ -21,6 +21,7 @@
 
 import android.os.BatteryManager;
 import android.os.BatteryUsageStats;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -34,9 +35,14 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class BatteryChargeCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
                     .setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 4000.0);
 
@@ -46,15 +52,17 @@
 
         final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
-                /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0,
-                1_000_000, 1_000_000, 1_000_000);
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
-                /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0,
-                1_500_000, 1_500_000, 1_500_000);
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
-                /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0,
-                2_000_000, 2_000_000, 2_000_000);
+        synchronized (batteryStats) {
+            batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+                    /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0,
+                    1_000_000, 1_000_000, 1_000_000);
+            batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+                    /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0,
+                    1_500_000, 1_500_000, 1_500_000);
+            batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+                    /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0,
+                    2_000_000, 2_000_000, 2_000_000);
+        }
 
         mStatsRule.setTime(5_000_000, 5_000_000);
         BatteryChargeCalculator calculator = new BatteryChargeCalculator();
@@ -73,10 +81,11 @@
         assertThat(batteryUsageStats.getChargeTimeRemainingMs()).isEqualTo(-1);
 
         // Plug in
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_CHARGING, 100,
-                BatteryManager.BATTERY_PLUGGED_USB, 80, 72, 3700, 2_400_000, 4_000_000, 100,
-                4_000_000, 4_000_000, 4_000_000);
-
+        synchronized (batteryStats) {
+            batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_CHARGING, 100,
+                    BatteryManager.BATTERY_PLUGGED_USB, 80, 72, 3700, 2_400_000, 4_000_000, 100,
+                    4_000_000, 4_000_000, 4_000_000);
+        }
         batteryUsageStats = mStatsRule.apply(calculator);
 
         assertThat(batteryUsageStats.getChargeTimeRemainingMs()).isEqualTo(100_000);
@@ -86,15 +95,17 @@
     public void testDischargeTotals_chargeUahUnavailable() {
         final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
-                /* plugType */ 0, 90, 72, 3700, 0, 0, 0,
-                1_000_000, 1_000_000, 1_000_000);
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
-                /* plugType */ 0, 85, 72, 3700, 0, 0, 0,
-                1_500_000, 1_500_000, 1_500_000);
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
-                /* plugType */ 0, 80, 72, 3700, 0, 0, 0,
-                2_000_000, 2_000_000, 2_000_000);
+        synchronized (batteryStats) {
+            batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+                    /* plugType */ 0, 90, 72, 3700, 0, 0, 0,
+                    1_000_000, 1_000_000, 1_000_000);
+            batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+                    /* plugType */ 0, 85, 72, 3700, 0, 0, 0,
+                    1_500_000, 1_500_000, 1_500_000);
+            batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+                    /* plugType */ 0, 80, 72, 3700, 0, 0, 0,
+                    2_000_000, 2_000_000, 2_000_000);
+        }
 
         BatteryChargeCalculator calculator = new BatteryChargeCalculator();
         BatteryUsageStats batteryUsageStats = mStatsRule.apply(calculator);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
index 9c2834d..997b771 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
@@ -215,7 +215,7 @@
 
     public class TestBatteryStatsImpl extends BatteryStatsImpl {
         public TestBatteryStatsImpl(Context context) {
-            super(Clock.SYSTEM_CLOCK, null, null, null);
+            super(Clock.SYSTEM_CLOCK, null, null, null, null, null, null);
             mPowerProfile = new PowerProfile(context, true /* forTest */);
 
             SparseArray<int[]> cpusByPolicy = new SparseArray<>();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java
index f9f32b2..6e62147 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java
@@ -40,6 +40,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.SparseArray;
 import android.util.SparseLongArray;
 import android.view.Display;
@@ -56,6 +57,7 @@
 import com.android.internal.util.ArrayUtils;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -84,7 +86,13 @@
  */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@SuppressWarnings("SynchronizeOnNonFinalField")
 public class BatteryStatsCpuTimesTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     @Mock
     KernelCpuUidUserSysTimeReader mCpuUidUserSysTimeReader;
     @Mock
@@ -128,7 +136,9 @@
         initKernelCpuSpeedReaders(numClusters);
 
         // RUN
-        mBatteryStatsImpl.updateCpuTimeLocked(false, false, null);
+        synchronized (mBatteryStatsImpl) {
+            mBatteryStatsImpl.updateCpuTimeLocked(false, false, null);
+        }
 
         // VERIFY
         verify(mCpuUidUserSysTimeReader).readDelta(anyBoolean(), isNull());
@@ -147,7 +157,9 @@
         mBatteryStatsImpl.setOnBatteryInternal(true);
 
         // RUN
-        mBatteryStatsImpl.updateCpuTimeLocked(true, false, null);
+        synchronized (mBatteryStatsImpl) {
+            mBatteryStatsImpl.updateCpuTimeLocked(true, false, null);
+        }
 
         // VERIFY
         verify(mUserInfoProvider).refreshUserIds();
@@ -239,7 +251,7 @@
         mBatteryStatsImpl.updateClusterSpeedTimes(updatedUids, true, null);
 
         // VERIFY
-        int totalClustersTimeMs = 0;
+        long totalClustersTimeMs = 0;
         for (int i = 0; i < clusterSpeedTimesMs.length; ++i) {
             for (int j = 0; j < clusterSpeedTimesMs[i].length; ++j) {
                 totalClustersTimeMs += clusterSpeedTimesMs[i][j];
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
index bf5bf36..395b3aa 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
@@ -21,6 +21,7 @@
 import android.os.BatteryManager;
 import android.os.BatteryStats;
 import android.os.Process;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -29,6 +30,7 @@
 import com.android.internal.os.MonotonicClock;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -42,6 +44,11 @@
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class BatteryStatsHistoryIteratorTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
 
     private final MockClock mMockClock = new MockClock();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index 9251376..c58c92b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -26,7 +26,6 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
-import android.content.Context;
 import android.os.BatteryConsumer;
 import android.os.BatteryManager;
 import android.os.BatteryStats;
@@ -39,7 +38,6 @@
 import android.util.AtomicFile;
 import android.util.Log;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.os.BatteryStatsHistory;
@@ -59,6 +57,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -80,13 +79,14 @@
     private BatteryStatsHistory.TraceDelegate mTracer;
     @Mock
     private BatteryStatsHistory.HistoryStepDetailsCalculator mStepDetailsCalculator;
+    @Mock
+    private BatteryStatsHistory.EventLogger mEventLogger;
     private List<String> mReadFiles = new ArrayList<>();
 
     @Before
-    public void setUp() {
+    public void setUp() throws IOException {
         MockitoAnnotations.initMocks(this);
-        Context context = InstrumentationRegistry.getContext();
-        mSystemDir = context.getDataDir();
+        mSystemDir = Files.createTempDirectory("BatteryStatsHistoryTest").toFile();
         mHistoryDir = new File(mSystemDir, "battery-history");
         String[] files = mHistoryDir.list();
         if (files != null) {
@@ -99,7 +99,7 @@
         mClock.realtime = 123;
 
         mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
-                mStepDetailsCalculator, mClock, mMonotonicClock, mTracer);
+                mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger);
 
         when(mStepDetailsCalculator.getHistoryStepDetails())
                 .thenReturn(new BatteryStats.HistoryStepDetails());
@@ -238,7 +238,7 @@
 
         // create a new BatteryStatsHistory object, it will pick up existing history files.
         BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
-                null, mClock, mMonotonicClock, mTracer);
+                null, mClock, mMonotonicClock, mTracer, mEventLogger);
         // verify constructor can pick up all files from file system.
         verifyFileNames(history2, fileList);
         verifyActiveFile(history2, "33000.bh");
@@ -534,7 +534,7 @@
         // Keep the preserved part of history short - we only need to capture the very tail of
         // history.
         mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 1, 6000,
-                mStepDetailsCalculator, mClock, mMonotonicClock, mTracer);
+                mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger);
 
         mHistory.forceRecordAllHistory();
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 8d51592..548fae7 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -31,15 +31,18 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.bluetooth.BluetoothActivityEnergyInfo;
 import android.bluetooth.UidTraffic;
 import android.content.Context;
+import android.hardware.SensorManager;
 import android.os.BatteryConsumer;
 import android.os.BatteryManager;
 import android.os.BatteryStats;
@@ -51,11 +54,11 @@
 import android.os.Parcel;
 import android.os.WakeLockStats;
 import android.os.WorkSource;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.SparseArray;
 import android.view.Display;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.os.CpuScalingPolicies;
@@ -68,19 +71,26 @@
 import com.google.common.truth.LongSubject;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.time.Instant;
 import java.util.List;
 
-@LargeTest
 @RunWith(AndroidJUnit4.class)
 @SuppressWarnings("GuardedBy")
 public class BatteryStatsImplTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     @Mock
     private KernelCpuUidFreqTimeReader mKernelUidCpuFreqTimeReader;
     @Mock
@@ -110,7 +120,7 @@
     private PowerStatsExporter mPowerStatsExporter;
 
     @Before
-    public void setUp() {
+    public void setUp() throws IOException {
         MockitoAnnotations.initMocks(this);
 
         when(mKernelUidCpuFreqTimeReader.isFastCpuTimesReader()).thenReturn(true);
@@ -128,8 +138,17 @@
                 .setKernelSingleUidTimeReader(mKernelSingleUidTimeReader)
                 .setKernelWakelockReader(mKernelWakelockReader);
 
-        final Context context = InstrumentationRegistry.getContext();
-        File systemDir = context.getCacheDir();
+        File systemDir = Files.createTempDirectory("BatteryStatsHistoryTest").toFile();
+
+        Context context;
+        if (RavenwoodRule.isUnderRavenwood()) {
+            context = mock(Context.class);
+            SensorManager sensorManager = mock(SensorManager.class);
+            when(sensorManager.getSensorList(anyInt())).thenReturn(List.of());
+            when(context.getSystemService(SensorManager.class)).thenReturn(sensorManager);
+        } else {
+            context = InstrumentationRegistry.getContext();
+        }
         mPowerStatsStore = new PowerStatsStore(systemDir, mHandler,
                 new AggregatedPowerStatsConfig());
         mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mPowerStatsExporter,
@@ -747,14 +766,22 @@
     }
 
     private UidTraffic createUidTraffic(int appUid, long rxBytes, long txBytes) {
-        final Parcel parcel = Parcel.obtain();
-        parcel.writeInt(appUid); // mAppUid
-        parcel.writeLong(rxBytes); // mRxBytes
-        parcel.writeLong(txBytes); // mTxBytes
-        parcel.setDataPosition(0);
-        UidTraffic uidTraffic = UidTraffic.CREATOR.createFromParcel(parcel);
-        parcel.recycle();
-        return uidTraffic;
+        if (RavenwoodRule.isUnderRavenwood()) {
+            UidTraffic uidTraffic = mock(UidTraffic.class);
+            when(uidTraffic.getUid()).thenReturn(appUid);
+            when(uidTraffic.getRxBytes()).thenReturn(rxBytes);
+            when(uidTraffic.getTxBytes()).thenReturn(txBytes);
+            return uidTraffic;
+        } else {
+            final Parcel parcel = Parcel.obtain();
+            parcel.writeInt(appUid); // mAppUid
+            parcel.writeLong(rxBytes); // mRxBytes
+            parcel.writeLong(txBytes); // mTxBytes
+            parcel.setDataPosition(0);
+            UidTraffic uidTraffic = UidTraffic.CREATOR.createFromParcel(parcel);
+            parcel.recycle();
+            return uidTraffic;
+        }
     }
 
     private BluetoothActivityEnergyInfo createBluetoothActivityEnergyInfo(
@@ -764,21 +791,31 @@
             long controllerIdleTimeMs,
             long controllerEnergyUsed,
             UidTraffic... uidTraffic) {
-        Parcel parcel = Parcel.obtain();
-        parcel.writeLong(timestamp); // mTimestamp
-        parcel.writeInt(
-                BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE); // mBluetoothStackState
-        parcel.writeLong(controllerTxTimeMs); // mControllerTxTimeMs;
-        parcel.writeLong(controllerRxTimeMs); // mControllerRxTimeMs;
-        parcel.writeLong(controllerIdleTimeMs); // mControllerIdleTimeMs;
-        parcel.writeLong(controllerEnergyUsed); // mControllerEnergyUsed;
-        parcel.writeTypedList(ImmutableList.copyOf(uidTraffic)); // mUidTraffic
-        parcel.setDataPosition(0);
+        if (RavenwoodRule.isUnderRavenwood()) {
+            BluetoothActivityEnergyInfo info = mock(BluetoothActivityEnergyInfo.class);
+            when(info.getTimestampMillis()).thenReturn(timestamp);
+            when(info.getControllerTxTimeMillis()).thenReturn(controllerTxTimeMs);
+            when(info.getControllerRxTimeMillis()).thenReturn(controllerRxTimeMs);
+            when(info.getControllerIdleTimeMillis()).thenReturn(controllerIdleTimeMs);
+            when(info.getControllerEnergyUsed()).thenReturn(controllerEnergyUsed);
+            when(info.getUidTraffic()).thenReturn(ImmutableList.copyOf(uidTraffic));
+            return info;
+        } else {
+            Parcel parcel = Parcel.obtain();
+            parcel.writeLong(timestamp); // mTimestamp
+            parcel.writeInt(BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE);
+            parcel.writeLong(controllerTxTimeMs); // mControllerTxTimeMs;
+            parcel.writeLong(controllerRxTimeMs); // mControllerRxTimeMs;
+            parcel.writeLong(controllerIdleTimeMs); // mControllerIdleTimeMs;
+            parcel.writeLong(controllerEnergyUsed); // mControllerEnergyUsed;
+            parcel.writeTypedList(ImmutableList.copyOf(uidTraffic)); // mUidTraffic
+            parcel.setDataPosition(0);
 
-        BluetoothActivityEnergyInfo info =
-                BluetoothActivityEnergyInfo.CREATOR.createFromParcel(parcel);
-        parcel.recycle();
-        return info;
+            BluetoothActivityEnergyInfo info =
+                    BluetoothActivityEnergyInfo.CREATOR.createFromParcel(parcel);
+            parcel.recycle();
+            return info;
+        }
     }
 
     @Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
index eea2875..07cefa9 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
@@ -28,6 +28,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 
 import android.app.ActivityManager;
@@ -40,6 +44,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.WorkSource;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.telephony.AccessNetworkConstants;
 import android.telephony.ActivityStatsTechSpecificInfo;
 import android.telephony.Annotation;
@@ -50,7 +55,6 @@
 import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
 import android.util.Log;
-import android.util.MutableInt;
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.view.Display;
@@ -63,8 +67,8 @@
 import com.android.internal.power.EnergyConsumerStats;
 import com.android.server.power.stats.BatteryStatsImpl.DualTimer;
 
-import junit.framework.TestCase;
-
+import org.junit.Rule;
+import org.junit.Test;
 import org.mockito.Mock;
 
 import java.util.ArrayList;
@@ -78,7 +82,14 @@
  * Test various BatteryStatsImpl noteStart methods.
  */
 @SuppressWarnings("GuardedBy")
-public class BatteryStatsNoteTest extends TestCase {
+@SmallTest
+public class BatteryStatsNoteTest {
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final String TAG = BatteryStatsNoteTest.class.getSimpleName();
 
     private static final int UID = 10500;
@@ -96,7 +107,7 @@
     /**
      * Test BatteryStatsImpl.Uid.noteBluetoothScanResultLocked.
      */
-    @SmallTest
+    @Test
     public void testNoteBluetoothScanResultLocked() throws Exception {
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(new MockClock());
         bi.updateTimeBasesLocked(true, Display.STATE_OFF, 0, 0);
@@ -125,7 +136,7 @@
     /**
      * Test BatteryStatsImpl.Uid.noteStartWakeLocked.
      */
-    @SmallTest
+    @Test
     public void testNoteStartWakeLocked() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -155,7 +166,7 @@
     /**
      * Test BatteryStatsImpl.Uid.noteStartWakeLocked for an isolated uid.
      */
-    @SmallTest
+    @Test
     public void testNoteStartWakeLocked_isolatedUid() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         PowerStatsUidResolver uidResolver = new PowerStatsUidResolver();
@@ -197,7 +208,7 @@
      * Test BatteryStatsImpl.Uid.noteStartWakeLocked for an isolated uid, with a race where the
      * isolated uid is removed from batterystats before the wakelock has been stopped.
      */
-    @SmallTest
+    @Test
     public void testNoteStartWakeLocked_isolatedUidRace() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         PowerStatsUidResolver uidResolver = new PowerStatsUidResolver();
@@ -241,7 +252,7 @@
     /**
      * Test BatteryStatsImpl.Uid.noteLongPartialWakelockStart for an isolated uid.
      */
-    @SmallTest
+    @Test
     public void testNoteLongPartialWakelockStart_isolatedUid() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         PowerStatsUidResolver uidResolver = new PowerStatsUidResolver();
@@ -296,7 +307,7 @@
     /**
      * Test BatteryStatsImpl.Uid.noteLongPartialWakelockStart for an isolated uid.
      */
-    @SmallTest
+    @Test
     public void testNoteLongPartialWakelockStart_isolatedUidRace() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         PowerStatsUidResolver uidResolver = new PowerStatsUidResolver();
@@ -354,7 +365,7 @@
     /**
      * Test BatteryStatsImpl.noteUidProcessStateLocked.
      */
-    @SmallTest
+    @Test
     public void testNoteUidProcessStateLocked() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -441,7 +452,7 @@
     /**
      * Test BatteryStatsImpl.updateTimeBasesLocked.
      */
-    @SmallTest
+    @Test
     public void testUpdateTimeBasesLocked() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -465,7 +476,7 @@
     /**
      * Test BatteryStatsImpl.noteScreenStateLocked sets timebases and screen states correctly.
      */
-    @SmallTest
+    @Test
     public void testNoteScreenStateLocked() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -512,7 +523,7 @@
      * Test BatteryStatsImpl.noteScreenStateLocked sets timebases and screen states correctly for
      * multi display devices
      */
-    @SmallTest
+    @Test
     public void testNoteScreenStateLocked_multiDisplay() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -606,7 +617,7 @@
      * Off             -------       ----------------------
      * Doze                                ----------------
      */
-    @SmallTest
+    @Test
     public void testNoteScreenStateTimersLocked() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -656,7 +667,7 @@
      * Test BatteryStatsImpl.noteScreenStateLocked updates timers correctly for multi display
      * devices.
      */
-    @SmallTest
+    @Test
     public void testNoteScreenStateTimersLocked_multiDisplay() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -807,7 +818,7 @@
     /**
      * Test BatteryStatsImpl.noteScreenBrightnessLocked updates timers correctly.
      */
-    @SmallTest
+    @Test
     public void testScreenBrightnessLocked_multiDisplay() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -929,7 +940,7 @@
         checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
     }
 
-    @SmallTest
+    @Test
     public void testAlarmStartAndFinishLocked() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -967,7 +978,7 @@
         assertThat(iterator.next()).isNull();
     }
 
-    @SmallTest
+    @Test
     public void testAlarmStartAndFinishLocked_workSource() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1013,7 +1024,7 @@
         assertEquals(500, item.eventTag.uid);
     }
 
-    @SmallTest
+    @Test
     public void testNoteWakupAlarmLocked() {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1031,7 +1042,7 @@
         assertEquals(1, pkg.getWakeupAlarmStats().size());
     }
 
-    @SmallTest
+    @Test
     public void testNoteWakupAlarmLocked_workSource_uid() {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1064,7 +1075,7 @@
         assertEquals(1, pkg.getWakeupAlarmStats().size());
     }
 
-    @SmallTest
+    @Test
     public void testNoteWakupAlarmLocked_workSource_workChain() {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1090,7 +1101,7 @@
         assertEquals(0, pkg.getWakeupAlarmStats().size());
     }
 
-    @SmallTest
+    @Test
     public void testNoteGpsChanged() {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1114,7 +1125,7 @@
         assertFalse(t.isRunningLocked());
     }
 
-    @SmallTest
+    @Test
     public void testNoteGpsChanged_workSource() {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1138,7 +1149,7 @@
         assertFalse(t.isRunningLocked());
     }
 
-    @SmallTest
+    @Test
     public void testUpdateDisplayMeasuredEnergyStatsLocked() {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1223,7 +1234,7 @@
         checkMeasuredCharge("H", uid1, blame1, uid2, blame2, globalDoze, bi);
     }
 
-    @SmallTest
+    @Test
     public void testUpdateCustomMeasuredEnergyStatsLocked_neverCalled() {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1237,7 +1248,7 @@
         checkCustomBatteryConsumption("0", 0, 0, uid1, 0, 0, uid2, 0, 0, bi);
     }
 
-    @SmallTest
+    @Test
     public void testUpdateCustomMeasuredEnergyStatsLocked() {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1314,7 +1325,7 @@
                 "D", totalBlameA, totalBlameB, uid1, blame1A, blame1B, uid2, blame2A, blame2B, bi);
     }
 
-    @SmallTest
+    @Test
     public void testGetPerStateActiveRadioDurationMs_noModemActivity() {
         final MockClock clock = new MockClock(); // holds realtime and uptime in ms
         final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
@@ -1470,7 +1481,7 @@
                 expectedTxDurationsMs, bi, state.currentTimeMs);
     }
 
-    @SmallTest
+    @Test
     public void testGetPerStateActiveRadioDurationMs_initialModemActivity() {
         final MockClock clock = new MockClock(); // holds realtime and uptime in ms
         final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
@@ -1612,7 +1623,7 @@
                 expectedTxDurationsMs, bi, state.currentTimeMs);
     }
 
-    @SmallTest
+    @Test
     public void testGetPerStateActiveRadioDurationMs_withModemActivity() {
         final MockClock clock = new MockClock(); // holds realtime and uptime in ms
         final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
@@ -1852,7 +1863,7 @@
                 expectedTxDurationsMs, bi, state.currentTimeMs);
     }
 
-    @SmallTest
+    @Test
     public void testGetPerStateActiveRadioDurationMs_withSpecificInfoModemActivity() {
         final MockClock clock = new MockClock(); // holds realtime and uptime in ms
         final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
@@ -2145,19 +2156,19 @@
                 expectedTxDurationsMs, bi, state.currentTimeMs);
     }
 
-    @SmallTest
+    @Test
     @SuppressWarnings("GuardedBy")
     public void testProcStateSyncScheduling_mobileRadioActiveState() {
         final MockClock clock = new MockClock(); // holds realtime and uptime in ms
         final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
-        final MutableInt lastProcStateChangeFlags = new MutableInt(0);
+        final int[] lastProcStateChangeFlags = new int[1];
 
         MockBatteryStatsImpl.DummyExternalStatsSync externalStatsSync =
                 new MockBatteryStatsImpl.DummyExternalStatsSync() {
                     @Override
                     public void scheduleSyncDueToProcessStateChange(int flags,
                             long delayMillis) {
-                        lastProcStateChangeFlags.value = flags;
+                        lastProcStateChangeFlags[0] = flags;
                     }
                 };
 
@@ -2170,19 +2181,19 @@
         bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr,
                 UID);
 
-        lastProcStateChangeFlags.value = 0;
+        lastProcStateChangeFlags[0] = 0;
         clock.realtime = clock.uptime = 2002;
         bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND);
 
         final int allProcFlags = BatteryStatsImpl.ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE;
-        assertEquals(allProcFlags, lastProcStateChangeFlags.value);
+        assertEquals(allProcFlags, lastProcStateChangeFlags[0]);
 
         // Note mobile radio is off.
         curr = 1000L * (clock.realtime = clock.uptime = 3003);
         bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, curr,
                 UID);
 
-        lastProcStateChangeFlags.value = 0;
+        lastProcStateChangeFlags[0] = 0;
         clock.realtime = clock.uptime = 4004;
         bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
 
@@ -2190,10 +2201,10 @@
                 & ~BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO;
         assertEquals(
                 "An inactive radio should not be queried on proc state change",
-                noRadioProcFlags, lastProcStateChangeFlags.value);
+                noRadioProcFlags, lastProcStateChangeFlags[0]);
     }
 
-    @SmallTest
+    @Test
     public void testNoteMobileRadioPowerStateLocked() {
         long curr;
         boolean update;
@@ -2243,7 +2254,7 @@
                 update);
     }
 
-    @SmallTest
+    @Test
     public void testNoteMobileRadioPowerStateLocked_rateLimited() {
         long curr;
         boolean update;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java
index 9c70f37..96780c3 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java
@@ -16,24 +16,36 @@
 
 package com.android.server.power.stats;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
 import android.app.ActivityManager;
 import android.os.BatteryStats;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.view.Display;
 
 import androidx.test.filters.SmallTest;
 
-import junit.framework.TestCase;
+import org.junit.Rule;
+import org.junit.Test;
 
 /**
  * Test BatteryStatsImpl Sensor Timers.
  */
-public class BatteryStatsSensorTest extends TestCase {
+@SmallTest
+public class BatteryStatsSensorTest {
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
 
     private static final int UID = 10500;
     private static final int UID_2 = 10501; // second uid for testing pool usage
     private static final int SENSOR_ID = -10000;
 
-    @SmallTest
+    @Test
     public void testSensorStartStop() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -70,7 +82,7 @@
                 clocks.realtime * 1000, BatteryStats.STATS_SINCE_CHARGED));
     }
 
-    @SmallTest
+    @Test
     public void testCountingWhileOffBattery() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -106,7 +118,7 @@
         assertEquals(0, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED));
     }
 
-    @SmallTest
+    @Test
     public void testCountingWhileOnBattery() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -141,7 +153,7 @@
         assertEquals(1, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED));
     }
 
-    @SmallTest
+    @Test
     public void testBatteryStatusOnToOff() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -187,7 +199,7 @@
                 sensorTimer.getTotalTimeLocked(curr, BatteryStats.STATS_SINCE_CHARGED));
     }
 
-    @SmallTest
+    @Test
     public void testBatteryStatusOffToOn() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -238,7 +250,7 @@
         assertEquals(0, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED));
     }
 
-    @SmallTest
+    @Test
     public void testPooledBackgroundUsage() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -375,7 +387,7 @@
         assertEquals(2, bgTimer2.getCountLocked(BatteryStats.STATS_SINCE_CHARGED));
     }
 
-    @SmallTest
+    @Test
     public void testSensorReset() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -419,7 +431,7 @@
         assertNull(sensor);
     }
 
-    @SmallTest
+    @Test
     public void testSensorResetTimes() throws Exception {
         final MockClock clocks = new MockClock();
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java
index 200eb1d..6f683ae 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java
@@ -18,16 +18,25 @@
 
 import android.os.BatteryStats;
 import android.os.Parcel;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 
 import junit.framework.Assert;
-import junit.framework.TestCase;
+
+import org.junit.Rule;
+import org.junit.Test;
 
 /**
  * Provides test cases for android.os.BatteryStats.
  */
-public class BatteryStatsServTest extends TestCase {
+@SmallTest
+public class BatteryStatsServTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final String TAG = "BatteryStatsServTest";
 
     public static class TestServ extends BatteryStatsImpl.Uid.Pkg.Serv {
@@ -90,7 +99,7 @@
     /**
      * Test that the constructor and detach methods touch the time bast observer list.
      */
-    @SmallTest
+    @Test
     public void testConstructAndDetach() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
 
@@ -104,7 +113,7 @@
     /**
      * Test parceling and unparceling.
      */
-    @SmallTest
+    @Test
     public void testParceling() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ orig = new TestServ(bsi);
@@ -133,7 +142,7 @@
     /**
      * Test getLaunchTimeToNow()
      */
-    @SmallTest
+    @Test
     public void testLaunchTimeToNow() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -151,7 +160,7 @@
     /**
      * Test getStartTimeToNow()
      */
-    @SmallTest
+    @Test
     public void testStartTimeToNow() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -168,7 +177,7 @@
     /**
      * Test startLaunchedLocked while not previously launched
      */
-    @SmallTest
+    @Test
     public void testStartLaunchedLockedWhileLaunched() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
             @Override
@@ -197,7 +206,7 @@
     /**
      * Test startLaunchedLocked while previously launched
      */
-    @SmallTest
+    @Test
     public void testStartLaunchedLockedWhileNotLaunched() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
             @Override
@@ -224,7 +233,7 @@
     /**
      * Test stopLaunchedLocked when not previously launched.
      */
-    @SmallTest
+    @Test
     public void testStopLaunchedLockedWhileNotLaunched() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
             @Override
@@ -254,7 +263,7 @@
      * Test stopLaunchedLocked when previously launched, with measurable time between
      * start and stop.
      */
-    @SmallTest
+    @Test
     public void testStopLaunchedLockedWhileLaunchedNormal() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
             @Override
@@ -283,7 +292,7 @@
      * Test stopLaunchedLocked when previously launched, with no measurable time between
      * start and stop.
      */
-    @SmallTest
+    @Test
     public void testStopLaunchedLockedWhileLaunchedTooQuick() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -306,7 +315,7 @@
     /**
      * Test startRunningLocked while previously running
      */
-    @SmallTest
+    @Test
     public void testStartRunningLockedWhileRunning() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
             @Override
@@ -335,7 +344,7 @@
     /**
      * Test startRunningLocked while not previously launched
      */
-    @SmallTest
+    @Test
     public void testStartRunningLockedWhileNotRunning() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
             @Override
@@ -364,7 +373,7 @@
      * Test stopRunningLocked when previously launched, with measurable time between
      * start and stop.
      */
-    @SmallTest
+    @Test
     public void testStopRunningLockedWhileRunningNormal() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
             @Override
@@ -393,7 +402,7 @@
      * Test stopRunningLocked when previously launched, with measurable time between
      * start and stop.
      */
-    @SmallTest
+    @Test
     public void testStopRunningLockedWhileRunningTooQuick() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -416,7 +425,7 @@
     /**
      * Test that getBatteryStats returns the BatteryStatsImpl passed in to the contstructor.
      */
-    @SmallTest
+    @Test
     public void testGetBatteryStats() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -427,7 +436,7 @@
     /**
      * Test getLaunches
      */
-    @SmallTest
+    @Test
     public void testGetLaunches() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -449,7 +458,7 @@
     /**
      * Test getStartTime while running
      */
-    @SmallTest
+    @Test
     public void testGetStartTimeRunning() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -475,7 +484,7 @@
     /**
      * Test getStartTime while not running
      */
-    @SmallTest
+    @Test
     public void testGetStartTimeNotRunning() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -502,7 +511,7 @@
     /**
      * Test getStarts
      */
-    @SmallTest
+    @Test
     public void testGetStarts() throws Exception  {
         MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
         TestServ serv = new TestServ(bsi);
@@ -521,4 +530,3 @@
         Assert.assertEquals(8085, serv.getLaunches());
     }
 }
-
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
index face849..05d8a00 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
@@ -46,6 +46,7 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.IgnoreUnderRavenwood
 public class BatteryStatsUserLifecycleTests {
 
     private static final long POLL_INTERVAL_MS = 500;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 2e0ba00..6cd79bc 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -23,6 +23,7 @@
 
 import android.app.ActivityManager;
 import android.content.Context;
+import android.hardware.SensorManager;
 import android.os.BatteryConsumer;
 import android.os.BatteryManager;
 import android.os.BatteryStats;
@@ -32,6 +33,7 @@
 import android.os.Parcel;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -40,36 +42,69 @@
 import com.android.internal.os.BatteryStatsHistoryIterator;
 import com.android.internal.os.PowerProfile;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.util.List;
-import java.util.Random;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class BatteryUsageStatsProviderTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
     private static final long MINUTE_IN_MS = 60 * 1000;
     private static final double PRECISION = 0.00001;
 
-    private final File mHistoryDir = createTemporaryDirectory();
+    private File mHistoryDir;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule =
             new BatteryUsageStatsRule(12345, mHistoryDir)
                     .setAveragePower(PowerProfile.POWER_FLASHLIGHT, 360.0)
                     .setAveragePower(PowerProfile.POWER_AUDIO, 720.0);
+
     private MockClock mMockClock = mStatsRule.getMockClock();
+    private Context mContext;
+
+    @Before
+    public void setup() throws IOException {
+        mHistoryDir = Files.createTempDirectory("BatteryUsageStatsProviderTest").toFile();
+        clearDirectory(mHistoryDir);
+
+        if (RavenwoodRule.isUnderRavenwood()) {
+            mContext = mock(Context.class);
+            SensorManager sensorManager = mock(SensorManager.class);
+            when(mContext.getSystemService(SensorManager.class)).thenReturn(sensorManager);
+        } else {
+            mContext = InstrumentationRegistry.getContext();
+        }
+    }
+
+    private void clearDirectory(File dir) {
+        if (dir.exists()) {
+            for (File child : dir.listFiles()) {
+                if (child.isDirectory()) {
+                    clearDirectory(child);
+                }
+                child.delete();
+            }
+        }
+    }
 
     @Test
     public void test_getBatteryUsageStats() {
         BatteryStatsImpl batteryStats = prepareBatteryStats();
 
-        Context context = InstrumentationRegistry.getContext();
-        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
                 mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
 
         final BatteryUsageStats batteryUsageStats =
@@ -105,8 +140,7 @@
     public void test_selectPowerComponents() {
         BatteryStatsImpl batteryStats = prepareBatteryStats();
 
-        Context context = InstrumentationRegistry.getContext();
-        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
                 mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
 
         final BatteryUsageStats batteryUsageStats =
@@ -211,8 +245,7 @@
             batteryStats.noteAlarmFinishLocked("foo", null, APP_UID, 3_001_000, 2_001_000);
         }
 
-        Context context = InstrumentationRegistry.getContext();
-        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
                 mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
 
         final BatteryUsageStats batteryUsageStats =
@@ -300,8 +333,7 @@
             }
         }
 
-        Context context = InstrumentationRegistry.getContext();
-        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
                 mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
 
         final BatteryUsageStats batteryUsageStats =
@@ -311,7 +343,9 @@
         Parcel parcel = Parcel.obtain();
         parcel.writeParcelable(batteryUsageStats, 0);
 
-        assertThat(parcel.dataSize()).isAtMost(128_000);
+        if (!RavenwoodRule.isUnderRavenwood()) {
+            assertThat(parcel.dataSize()).isAtMost(128_000);
+        }
 
         parcel.setDataPosition(0);
 
@@ -375,7 +409,6 @@
 
     @Test
     public void testAggregateBatteryStats() {
-        Context context = InstrumentationRegistry.getContext();
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
         setTime(5 * MINUTE_IN_MS);
@@ -384,11 +417,11 @@
         }
 
         PowerStatsStore powerStatsStore = new PowerStatsStore(
-                new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"),
+                new File(mHistoryDir, "powerstatsstore"),
                 mStatsRule.getHandler(), null);
         powerStatsStore.reset();
 
-        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
                 mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), powerStatsStore,
                 mMockClock);
 
@@ -485,7 +518,6 @@
 
     @Test
     public void testAggregateBatteryStats_incompatibleSnapshot() {
-        Context context = InstrumentationRegistry.getContext();
         MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
         batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
 
@@ -511,7 +543,7 @@
         when(powerStatsStore.loadPowerStatsSpan(1, BatteryUsageStatsSection.TYPE))
                 .thenReturn(span1);
 
-        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
                 mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), powerStatsStore,
                 mMockClock);
 
@@ -523,20 +555,4 @@
                 .isEqualTo(batteryStats.getCustomEnergyConsumerNames());
         assertThat(stats.getStatsDuration()).isEqualTo(1234);
     }
-
-    private static final Random sRandom = new Random();
-
-    /**
-     * Creates a unique new temporary directory under "java.io.tmpdir".
-     */
-    private static File createTemporaryDirectory() {
-        while (true) {
-            String candidateName =
-                    BatteryUsageStatsProviderTest.class.getSimpleName() + sRandom.nextInt();
-            File result = new File(System.getProperty("java.io.tmpdir"), candidateName);
-            if (result.mkdir()) {
-                return result;
-            }
-        }
-    }
 }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index ba2b538..8bdb029 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -49,6 +49,7 @@
 import java.io.File;
 import java.util.Arrays;
 
+@SuppressWarnings("SynchronizeOnNonFinalField")
 public class BatteryUsageStatsRule implements TestRule {
     public static final BatteryUsageStatsQuery POWER_PROFILE_MODEL_ONLY =
             new BatteryUsageStatsQuery.Builder()
@@ -71,6 +72,8 @@
     private int mDisplayCount = -1;
     private int mPerUidModemModel = -1;
     private NetworkStats mNetworkStats;
+    private boolean[] mSupportedStandardBuckets;
+    private String[] mCustomPowerComponentNames;
 
     public BatteryUsageStatsRule() {
         this(0, null);
@@ -102,6 +105,11 @@
         mBatteryStats = new MockBatteryStatsImpl(mMockClock, mHistoryDir, mHandler);
         mBatteryStats.setPowerProfile(mPowerProfile);
         mBatteryStats.setCpuScalingPolicies(new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy));
+        synchronized (mBatteryStats) {
+            mBatteryStats.initEnergyConsumerStatsLocked(mSupportedStandardBuckets,
+                    mCustomPowerComponentNames);
+        }
+        mBatteryStats.informThatAllExternalStatsAreFlushed();
 
         mBatteryStats.onSystemReady();
 
@@ -230,13 +238,15 @@
     /** Call only after setting the power profile information. */
     public BatteryUsageStatsRule initMeasuredEnergyStatsLocked(
             String[] customPowerComponentNames) {
-        final boolean[] supportedStandardBuckets =
-                new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
-        Arrays.fill(supportedStandardBuckets, true);
-        synchronized (mBatteryStats) {
-            mBatteryStats.initEnergyConsumerStatsLocked(supportedStandardBuckets,
-                    customPowerComponentNames);
-            mBatteryStats.informThatAllExternalStatsAreFlushed();
+        mCustomPowerComponentNames = customPowerComponentNames;
+        mSupportedStandardBuckets = new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
+        Arrays.fill(mSupportedStandardBuckets, true);
+        if (mBatteryStats != null) {
+            synchronized (mBatteryStats) {
+                mBatteryStats.initEnergyConsumerStatsLocked(mSupportedStandardBuckets,
+                        mCustomPowerComponentNames);
+                mBatteryStats.informThatAllExternalStatsAreFlushed();
+            }
         }
         return this;
     }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
index 079ea2c7..851cf4a 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
@@ -37,6 +37,7 @@
 import android.os.Parcel;
 import android.os.UidBatteryConsumer;
 import android.os.UserBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.Xml;
 
 import androidx.test.filters.SmallTest;
@@ -45,6 +46,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -62,6 +64,10 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class BatteryUsageStatsTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
 
     private static final int USER_ID = 42;
     private static final int APP_UID1 = 271;
@@ -115,12 +121,13 @@
         final Parcel parcel = Parcel.obtain();
         parcel.writeParcelable(outBatteryUsageStats, 0);
 
-        assertThat(parcel.dataSize()).isLessThan(2000);
+        // Under ravenwood this parcel is larger.  On a device, 2K would suffice
+        assertThat(parcel.dataSize()).isLessThan(128_000);
 
         parcel.setDataPosition(0);
 
         final BatteryUsageStats inBatteryUsageStats =
-                parcel.readParcelable(getClass().getClassLoader());
+                parcel.readParcelable(getClass().getClassLoader(), BatteryUsageStats.class);
         parcel.recycle();
 
         assertThat(inBatteryUsageStats.getUidBatteryConsumers()).hasSize(uidCount);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java
index 4d4337c1..fe6424f 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java
@@ -18,6 +18,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothActivityEnergyInfo;
 import android.bluetooth.UidTraffic;
@@ -28,6 +31,7 @@
 import android.os.Process;
 import android.os.UidBatteryConsumer;
 import android.os.WorkSource;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -46,10 +50,15 @@
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class BluetoothPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE, 10.0)
             .setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_RX, 50.0)
@@ -331,33 +340,53 @@
         assertThat(usageDurationMillis).isEqualTo(durationMs);
     }
 
-    private UidTraffic createUidTraffic(int uid, long traffic1, long traffic2) {
-        final Parcel uidTrafficParcel = Parcel.obtain();
-        uidTrafficParcel.writeInt(uid);
-        uidTrafficParcel.writeLong(traffic1);
-        uidTrafficParcel.writeLong(traffic2);
-        uidTrafficParcel.setDataPosition(0);
+    private UidTraffic createUidTraffic(int appUid, long rxBytes, long txBytes) {
+        if (RavenwoodRule.isUnderRavenwood()) {
+            UidTraffic uidTraffic = mock(UidTraffic.class);
+            when(uidTraffic.getUid()).thenReturn(appUid);
+            when(uidTraffic.getRxBytes()).thenReturn(rxBytes);
+            when(uidTraffic.getTxBytes()).thenReturn(txBytes);
+            return uidTraffic;
+        } else {
+            final Parcel uidTrafficParcel = Parcel.obtain();
+            uidTrafficParcel.writeInt(appUid);
+            uidTrafficParcel.writeLong(rxBytes);
+            uidTrafficParcel.writeLong(txBytes);
+            uidTrafficParcel.setDataPosition(0);
 
-        UidTraffic traffic = UidTraffic.CREATOR.createFromParcel(uidTrafficParcel);
-        uidTrafficParcel.recycle();
-        return traffic;
+            UidTraffic traffic = UidTraffic.CREATOR.createFromParcel(uidTrafficParcel);
+            uidTrafficParcel.recycle();
+            return traffic;
+        }
     }
 
     private BluetoothActivityEnergyInfo createBtEnergyInfo(long timestamp, int stackState,
             long txTime, long rxTime, long idleTime, long energyUsed, List<UidTraffic> traffic) {
-        final Parcel btActivityEnergyInfoParcel = Parcel.obtain();
-        btActivityEnergyInfoParcel.writeLong(timestamp);
-        btActivityEnergyInfoParcel.writeInt(stackState);
-        btActivityEnergyInfoParcel.writeLong(txTime);
-        btActivityEnergyInfoParcel.writeLong(rxTime);
-        btActivityEnergyInfoParcel.writeLong(idleTime);
-        btActivityEnergyInfoParcel.writeLong(energyUsed);
-        btActivityEnergyInfoParcel.writeTypedList(traffic);
-        btActivityEnergyInfoParcel.setDataPosition(0);
+        if (RavenwoodRule.isUnderRavenwood()) {
+            BluetoothActivityEnergyInfo info = mock(BluetoothActivityEnergyInfo.class);
+            when(info.getTimestampMillis()).thenReturn(timestamp);
+            when(info.getBluetoothStackState()).thenReturn(stackState);
+            when(info.getControllerTxTimeMillis()).thenReturn(txTime);
+            when(info.getControllerRxTimeMillis()).thenReturn(rxTime);
+            when(info.getControllerIdleTimeMillis()).thenReturn(idleTime);
+            when(info.getControllerEnergyUsed()).thenReturn(energyUsed);
+            when(info.getUidTraffic()).thenReturn(ImmutableList.copyOf(traffic));
+            return info;
+        } else {
+            final Parcel btActivityEnergyInfoParcel = Parcel.obtain();
+            btActivityEnergyInfoParcel.writeLong(timestamp);
+            btActivityEnergyInfoParcel.writeInt(stackState);
+            btActivityEnergyInfoParcel.writeLong(txTime);
+            btActivityEnergyInfoParcel.writeLong(rxTime);
+            btActivityEnergyInfoParcel.writeLong(idleTime);
+            btActivityEnergyInfoParcel.writeLong(energyUsed);
+            btActivityEnergyInfoParcel.writeTypedList(traffic);
+            btActivityEnergyInfoParcel.setDataPosition(0);
 
-        BluetoothActivityEnergyInfo info = BluetoothActivityEnergyInfo.CREATOR
-                .createFromParcel(btActivityEnergyInfoParcel);
-        btActivityEnergyInfoParcel.recycle();
-        return info;
+            BluetoothActivityEnergyInfo info = BluetoothActivityEnergyInfo.CREATOR
+                    .createFromParcel(btActivityEnergyInfoParcel);
+            btActivityEnergyInfoParcel.recycle();
+            return info;
+        }
     }
 }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java
index ccace40..29e2f5e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java
@@ -75,6 +75,7 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.IgnoreUnderRavenwood
 public class BstatsCpuTimesValidationTest {
     private static final String TAG = BstatsCpuTimesValidationTest.class.getSimpleName();
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
index 5fce32f0..7225f2d 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
@@ -21,6 +21,7 @@
 import android.os.BatteryConsumer;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -34,12 +35,17 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class CameraPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int APP1_UID = Process.FIRST_APPLICATION_UID + 42;
     private static final int APP2_UID = Process.FIRST_APPLICATION_UID + 43;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_CAMERA, 360.0)
             .initMeasuredEnergyStatsLocked();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java
index 993d834..5c0e268 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java
@@ -33,6 +33,7 @@
 
 import android.os.BatteryConsumer;
 import android.os.PersistableBundle;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.LongArray;
 
 import androidx.test.filters.SmallTest;
@@ -55,7 +56,12 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class CpuAggregatedPowerStatsProcessorTest {
-    @Rule
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720)
             .setCpuScalingPolicy(0, new int[]{0, 1}, new int[]{100, 200})
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java
index 888bc62..71a65c8 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java
@@ -32,6 +32,7 @@
 import android.os.BatteryUsageStatsQuery;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.SparseArray;
 
 import androidx.test.filters.SmallTest;
@@ -55,6 +56,11 @@
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class CpuPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42;
@@ -62,7 +68,7 @@
 
     private static final int NUM_CPU_FREQS = 2 + 2;  // 2 clusters * 2 freqs each
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720)
             .setCpuScalingPolicy(0, new int[]{0, 1}, new int[]{100, 200})
@@ -93,14 +99,13 @@
     private SystemServerCpuThreadReader mMockSystemServerCpuThreadReader;
     @Mock
     private KernelSingleUidTimeReader mMockKernelSingleUidTimeReader;
+    private boolean[] mSupportedPowerBuckets;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        final boolean[] supportedPowerBuckets =
-                new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
-        supportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true;
+        mSupportedPowerBuckets = new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
 
         when(mMockCpuUidFreqTimeReader.isFastCpuTimesReader()).thenReturn(true);
 
@@ -112,8 +117,7 @@
                 .setKernelCpuUidUserSysTimeReader(mMockKernelCpuUidUserSysTimeReader)
                 .setKernelCpuUidActiveTimeReader(mMockKerneCpuUidActiveTimeReader)
                 .setKernelSingleUidTimeReader(mMockKernelSingleUidTimeReader)
-                .setSystemServerCpuThreadReader(mMockSystemServerCpuThreadReader)
-                .initEnergyConsumerStatsLocked(supportedPowerBuckets, new String[0]);
+                .setSystemServerCpuThreadReader(mMockSystemServerCpuThreadReader);
     }
 
     @Test
@@ -216,6 +220,10 @@
 
     @Test
     public void testMeasuredEnergyBasedModel() {
+        mSupportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true;
+        mStatsRule.getBatteryStats().initEnergyConsumerStatsLocked(mSupportedPowerBuckets,
+                new String[0]);
+
         when(mMockUserInfoProvider.exists(anyInt())).thenReturn(true);
 
         when(mMockKernelCpuSpeedReaders[0].readDelta()).thenReturn(new long[]{1000, 2000});
@@ -397,6 +405,10 @@
 
     @Test
     public void testMeasuredEnergyBasedModel_perProcessState() {
+        mSupportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true;
+        mStatsRule.getBatteryStats().initEnergyConsumerStatsLocked(mSupportedPowerBuckets,
+                new String[0]);
+
         when(mMockUserInfoProvider.exists(anyInt())).thenReturn(true);
 
         when(mMockKernelCpuSpeedReaders[0].readDelta()).thenReturn(new long[]{1000, 2000});
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
index 38a5d19..cbce7e8 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
@@ -52,6 +52,7 @@
 
 @RunWith(AndroidJUnit4.class)
 @LargeTest
+@android.platform.test.annotations.IgnoreUnderRavenwood
 public class CpuPowerStatsCollectorValidationTest {
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
index 245faaf..4ab706e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
@@ -21,6 +21,7 @@
 import android.os.BatteryConsumer;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.SparseLongArray;
 
 import androidx.test.filters.SmallTest;
@@ -34,11 +35,16 @@
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class CustomEnergyConsumerPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .initMeasuredEnergyStatsLocked(new String[]{"CUSTOM_COMPONENT1", "CUSTOM_COMPONENT2"});
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java
index 0f85fdc..757025e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java
@@ -21,6 +21,7 @@
 import android.os.BatteryConsumer;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -34,11 +35,16 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class FlashlightPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_FLASHLIGHT, 360.0);
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java
index 3f2a6d0..3b5658c 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java
@@ -21,6 +21,7 @@
 import android.os.BatteryConsumer;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -35,12 +36,16 @@
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class GnssPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
     private static final double PRECISION = 0.00001;
 
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
     private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 222;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_GPS_ON, 360.0)
             .setAveragePower(PowerProfile.POWER_GPS_SIGNAL_QUALITY_BASED,
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java
index 3d150af..487d864 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.BatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -32,9 +33,14 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class IdlePowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_CPU_IDLE, 720.0)
             .setAveragePower(PowerProfile.POWER_CPU_SUSPEND, 360.0);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java
index 2edfc8e..e023866 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java
@@ -24,6 +24,7 @@
 
 import java.nio.charset.Charset;
 
+@android.platform.test.annotations.IgnoreUnderRavenwood
 public class KernelWakelockReaderTest extends TestCase {
     /**
      * Helper class that builds the mock Kernel module file /d/wakeup_sources.
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java
index 2e962c3..1807ac5 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java
@@ -24,7 +24,6 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.os.Parcel;
@@ -159,7 +158,7 @@
         // Test with detachIfReset=false
         mCounterArray.reset(false /* detachIfReset */);
         assertArrayEquals(ZEROES, mCounterArray.mCounts);
-        verifyZeroInteractions(mTimeBase);
+        verifyNoMoreInteractions(mTimeBase);
 
         updateCounts(COUNTS);
         // Test with detachIfReset=true
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java
index 0eac625..4b608e3 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java
@@ -24,7 +24,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.os.Parcel;
@@ -140,7 +139,7 @@
         // Test with detachIfReset=false
         mCounter.reset(false /* detachIfReset */);
         assertEquals(0, getCount());
-        verifyZeroInteractions(mTimeBase);
+        verifyNoMoreInteractions(mTimeBase);
 
         mCounter.addCountLocked(COUNT, true);
         assertEquals(COUNT, getCount());
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java
index 2cce449..3a27188 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.BatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -32,9 +33,14 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class MemoryPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_MEMORY, new double[] {360.0, 720.0, 1080.0});
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 78c4bac..9f06913 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -26,6 +26,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BatteryStatsHistory;
 import com.android.internal.os.Clock;
 import com.android.internal.os.CpuScalingPolicies;
 import com.android.internal.os.KernelCpuSpeedReader;
@@ -70,7 +71,9 @@
 
     MockBatteryStatsImpl(Clock clock, File historyDirectory, Handler handler,
             PowerStatsUidResolver powerStatsUidResolver) {
-        super(clock, historyDirectory, handler, powerStatsUidResolver);
+        super(clock, historyDirectory, handler, powerStatsUidResolver,
+                mock(FrameworkStatsLogger.class), mock(BatteryStatsHistory.TraceDelegate.class),
+                mock(BatteryStatsHistory.EventLogger.class));
         initTimersAndCounters();
         setMaxHistoryBuffer(128 * 1024);
 
@@ -107,7 +110,9 @@
     }
 
     public Queue<UidToRemove> getPendingRemovedUids() {
-        return mPendingRemovedUids;
+        synchronized (this) {
+            return mPendingRemovedUids;
+        }
     }
 
     public boolean isOnBattery() {
@@ -275,6 +280,10 @@
         mHandler = handler;
     }
 
+    @Override
+    protected void updateBatteryPropertiesLocked() {
+    }
+
     public static class DummyExternalStatsSync implements ExternalStatsSync {
         public int flags = 0;
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
index 22a7351..af5b462 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
@@ -60,7 +60,7 @@
     public void setup() throws ParseException {
         mHistory = new BatteryStatsHistory(32, 1024,
                 mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock,
-                mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class));
+                mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class), null);
 
         AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig();
         config.trackPowerComponent(TEST_POWER_COMPONENT)
@@ -178,7 +178,7 @@
     }
 
     @NonNull
-    private static CharSequence formatDateTime(long timeInMillis) {
+    private static String formatDateTime(long timeInMillis) {
         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
         format.getCalendar().setTimeZone(TimeZone.getTimeZone("GMT"));
         return format.format(new Date(timeInMillis));
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
index 3560a26..18d7b90 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
@@ -99,7 +99,7 @@
         mPowerStatsStore = new PowerStatsStore(storeDirectory, new TestHandler(), config);
         mHistory = new BatteryStatsHistory(Parcel.obtain(), storeDirectory, 0, 10000,
                 mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock,
-                mMonotonicClock, null);
+                mMonotonicClock, null, null);
         mPowerStatsAggregator = new PowerStatsAggregator(config, mHistory);
 
         mCpuStatsArrayLayout = new CpuPowerStatsCollector.CpuStatsArrayLayout();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java
index 3723079..88d4ea7 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java
@@ -25,6 +25,7 @@
 import android.os.BatteryConsumer;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.view.Display;
 
 import androidx.test.filters.SmallTest;
@@ -38,14 +39,18 @@
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class ScreenPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
     private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42;
     private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 43;
     private static final long MINUTE_IN_MS = 60 * 1000;
     private static final long MINUTE_IN_US = 60 * 1000 * 1000;
-    private static final long HOUR_IN_MS = 60 * MINUTE_IN_MS;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 0, 36.0)
             .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 0, 48.0)
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java
index 4745270..c01f05f 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java
@@ -27,6 +27,7 @@
 import android.os.BatteryConsumer;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -40,6 +41,11 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class SensorPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int SENSOR_HANDLE_1 = 1;
@@ -47,7 +53,7 @@
 
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule();
 
     @Test
@@ -60,10 +66,12 @@
                 .thenReturn(List.of(sensor1, sensor2));
 
         final BatteryStatsImpl stats = mStatsRule.getBatteryStats();
-        stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_1, 1000, 1000);
-        stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_1, 2000, 2000);
-        stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_2, 3000, 3000);
-        stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_2, 5000, 5000);
+        synchronized (stats) {
+            stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_1, 1000, 1000);
+            stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_1, 2000, 2000);
+            stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_2, 3000, 3000);
+            stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_2, 5000, 5000);
+        }
 
         SensorPowerCalculator calculator = new SensorPowerCalculator(sensorManager);
 
@@ -84,11 +92,20 @@
                 .isWithin(PRECISION).of(0.5);
     }
 
-    private Sensor createSensor(int handle, int type, double power) {
-        return new Sensor(new InputSensorInfo("name", "vendor", 0 /* version */,
-                handle, type, 100.0f /*maxRange */, 0.02f /* resolution */,
-                (float) power, 1000 /* minDelay */, 0 /* fifoReservedEventCount */,
-                0 /* fifoMaxEventCount */, "" /* stringType */, "" /* requiredPermission */,
-                0 /* maxDelay */, 0 /* flags */, 0 /* id */));
+    private Sensor createSensor(int handle, int type, float power) {
+        if (RavenwoodRule.isUnderRavenwood()) {
+            Sensor sensor = mock(Sensor.class);
+
+            when(sensor.getHandle()).thenReturn(handle);
+            when(sensor.getType()).thenReturn(type);
+            when(sensor.getPower()).thenReturn(power);
+            return sensor;
+        } else {
+            return new Sensor(new InputSensorInfo("name", "vendor", 0 /* version */,
+                    handle, type, 100.0f /*maxRange */, 0.02f /* resolution */,
+                    (float) power, 1000 /* minDelay */, 0 /* fifoReservedEventCount */,
+                    0 /* fifoMaxEventCount */, "" /* stringType */, "" /* requiredPermission */,
+                    0 /* maxDelay */, 0 /* flags */, 0 /* id */));
+        }
     }
 }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java
index f14745e..438f0ec 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java
@@ -25,6 +25,7 @@
 import android.os.UidBatteryConsumer;
 import android.os.UserBatteryConsumer;
 import android.os.UserHandle;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -36,6 +37,11 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class UserPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     public static final int USER1 = 0;
     public static final int USER2 = 1625;
 
@@ -43,7 +49,7 @@
     private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 272;
     private static final int APP_UID3 = Process.FIRST_APPLICATION_UID + 314;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule();
 
     @Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java
index f578aa3..b9b7101 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java
@@ -21,6 +21,7 @@
 import android.os.BatteryConsumer;
 import android.os.Process;
 import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -34,11 +35,16 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class VideoPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_VIDEO, 360.0);
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java
index f196185..5b7762d 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java
@@ -23,6 +23,7 @@
 import android.os.Process;
 import android.os.UidBatteryConsumer;
 import android.os.WorkSource;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -36,12 +37,17 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class WakelockPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
     private static final int APP_PID = 3145;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_CPU_IDLE, 360.0);
 
@@ -51,10 +57,12 @@
 
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
-        batteryStats.noteStartWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake", "",
-                BatteryStats.WAKE_TYPE_PARTIAL, true, 1000, 1000);
-        batteryStats.noteStopWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake", "",
-                BatteryStats.WAKE_TYPE_PARTIAL, 2000, 2000);
+        synchronized (batteryStats) {
+            batteryStats.noteStartWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake",
+                    "", BatteryStats.WAKE_TYPE_PARTIAL, true, 1000, 1000);
+            batteryStats.noteStopWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake",
+                    "", BatteryStats.WAKE_TYPE_PARTIAL, 2000, 2000);
+        }
 
         mStatsRule.setTime(10_000, 6_000);
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java
index 113be8b..8e221be 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java
@@ -23,6 +23,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 import android.app.usage.NetworkStatsManager;
 import android.net.NetworkCapabilities;
 import android.net.NetworkStats;
@@ -33,6 +36,7 @@
 import android.os.UidBatteryConsumer;
 import android.os.WorkSource;
 import android.os.connectivity.WifiActivityEnergyInfo;
+import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -44,10 +48,17 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 
+import java.util.List;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 @SuppressWarnings("GuardedBy")
 public class WifiPowerCalculatorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
     private static final double PRECISION = 0.00001;
 
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
@@ -55,7 +66,7 @@
     @Mock
     NetworkStatsManager mNetworkStatsManager;
 
-    @Rule
+    @Rule(order = 1)
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
             .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE, 360.0)
             .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX, 480.0)
@@ -64,6 +75,7 @@
             .setAveragePower(PowerProfile.POWER_WIFI_SCAN, 480.0)
             .setAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN, 720.0)
             .setAveragePower(PowerProfile.POWER_WIFI_ACTIVE, 1080.0)
+            .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE, 3700)
             .initMeasuredEnergyStatsLocked();
 
     /** Sets up a batterystats object with pre-populated network values. */
@@ -78,21 +90,54 @@
         return batteryStats;
     }
 
-    private NetworkStats buildNetworkStats(long elapsedRealtime, int rxBytes, int rxPackets,
-            int txBytes, int txPackets) {
-        return new NetworkStats(elapsedRealtime, 1)
-                .addEntry(new NetworkStats.Entry("wifi", APP_UID, 0, 0,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, rxPackets,
-                        txBytes, txPackets, 100))
-                .addEntry(new NetworkStats.Entry("wifi", Process.WIFI_UID, 0, 0,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1111, 111,
-                        2222, 22, 111));
+    private NetworkStats buildNetworkStats(long elapsedRealtime, long rxBytes, long rxPackets,
+            long txBytes, long txPackets) {
+        if (RavenwoodRule.isUnderRavenwood()) {
+            NetworkStats stats = mock(NetworkStats.class);
+//        when(stats.getElapsedRealtime()).thenReturn(elapsedRealtime);
+
+            NetworkStats.Entry entry1 = mock(NetworkStats.Entry.class);
+//        when(entry1.getIface()).thenReturn("wifi");
+            when(entry1.getUid()).thenReturn(APP_UID);
+            when(entry1.getMetered()).thenReturn(METERED_NO);
+            when(entry1.getRoaming()).thenReturn(ROAMING_NO);
+            when(entry1.getDefaultNetwork()).thenReturn(DEFAULT_NETWORK_NO);
+            when(entry1.getRxBytes()).thenReturn(rxBytes);
+            when(entry1.getRxPackets()).thenReturn(rxPackets);
+            when(entry1.getTxBytes()).thenReturn(txBytes);
+            when(entry1.getTxPackets()).thenReturn(txPackets);
+            when(entry1.getOperations()).thenReturn(100L);
+
+            NetworkStats.Entry entry2 = mock(NetworkStats.Entry.class);
+//        when(entry2.getIface()).thenReturn("wifi");
+            when(entry2.getUid()).thenReturn(Process.WIFI_UID);
+            when(entry2.getMetered()).thenReturn(METERED_NO);
+            when(entry2.getRoaming()).thenReturn(ROAMING_NO);
+            when(entry2.getDefaultNetwork()).thenReturn(DEFAULT_NETWORK_NO);
+            when(entry2.getRxBytes()).thenReturn(1111L);
+            when(entry2.getRxPackets()).thenReturn(111L);
+            when(entry2.getTxBytes()).thenReturn(2222L);
+            when(entry2.getTxPackets()).thenReturn(22L);
+            when(entry2.getOperations()).thenReturn(111L);
+
+            when(stats.iterator()).thenAnswer(inv->List.of(entry1, entry2).iterator());
+
+            return stats;
+        } else {
+            return new NetworkStats(elapsedRealtime, 1)
+                    .addEntry(new NetworkStats.Entry("wifi", APP_UID, 0, 0,
+                            METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, rxPackets,
+                            txBytes, txPackets, 100))
+                    .addEntry(new NetworkStats.Entry("wifi", Process.WIFI_UID, 0, 0,
+                            METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1111, 111,
+                            2222, 22, 111));
+        }
     }
 
     /** Sets up an WifiActivityEnergyInfo for ActivityController-model-based tests. */
     private WifiActivityEnergyInfo setupPowerControllerBasedModelEnergyNumbersInfo() {
-        return new WifiActivityEnergyInfo(10000,
-                WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000);
+        return buildWifiActivityEnergyInfo(10000L, WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE,
+                1000L, 2000L, 3000L, 4000L);
     }
 
     @Test
@@ -142,7 +187,7 @@
         uid.setProcessStateForTest(
                 BatteryStats.Uid.PROCESS_STATE_FOREGROUND, 1000);
 
-        batteryStats.updateWifiState(new WifiActivityEnergyInfo(2000,
+        batteryStats.updateWifiState(buildWifiActivityEnergyInfo(2000,
                         WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000),
                 POWER_DATA_UNAVAILABLE, 2000, 2000,
                 mNetworkStatsManager);
@@ -152,7 +197,7 @@
 
         mStatsRule.setNetworkStats(buildNetworkStats(4000, 5000, 200, 7000, 80));
 
-        batteryStats.updateWifiState(new WifiActivityEnergyInfo(4000,
+        batteryStats.updateWifiState(buildWifiActivityEnergyInfo(4000,
                         WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 5000, 6000, 7000, 8000),
                 POWER_DATA_UNAVAILABLE, 4000, 4000,
                 mNetworkStatsManager);
@@ -231,7 +276,7 @@
         uid.setProcessStateForTest(
                 BatteryStats.Uid.PROCESS_STATE_FOREGROUND, 1000);
 
-        batteryStats.updateWifiState(new WifiActivityEnergyInfo(2000,
+        batteryStats.updateWifiState(buildWifiActivityEnergyInfo(2000,
                         WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000),
                 1_000_000, 2000, 2000,
                 mNetworkStatsManager);
@@ -241,7 +286,7 @@
 
         mStatsRule.setNetworkStats(buildNetworkStats(4000, 5000, 200, 7000, 80));
 
-        batteryStats.updateWifiState(new WifiActivityEnergyInfo(4000,
+        batteryStats.updateWifiState(buildWifiActivityEnergyInfo(4000,
                         WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 5000, 6000, 7000, 8000),
                 5_000_000, 4000, 4000,
                 mNetworkStatsManager);
@@ -329,4 +374,43 @@
         assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_WIFI))
                 .isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
     }
+
+    private WifiActivityEnergyInfo buildWifiActivityEnergyInfo(long timeSinceBoot,
+            int stackState, long txDuration, long rxDuration, long scanDuration,
+            long idleDuration) {
+        if (RavenwoodRule.isUnderRavenwood()) {
+            WifiActivityEnergyInfo info = mock(WifiActivityEnergyInfo.class);
+            when(info.getTimeSinceBootMillis()).thenReturn(timeSinceBoot);
+            when(info.getStackState()).thenReturn(stackState);
+            when(info.getControllerTxDurationMillis()).thenReturn(txDuration);
+            when(info.getControllerRxDurationMillis()).thenReturn(rxDuration);
+            when(info.getControllerScanDurationMillis()).thenReturn(scanDuration);
+            when(info.getControllerIdleDurationMillis()).thenReturn(idleDuration);
+            long energy = calculateEnergyMicroJoules(txDuration, rxDuration, idleDuration);
+            when(info.getControllerEnergyUsedMicroJoules()).thenReturn(energy);
+            return info;
+        } else {
+            return new WifiActivityEnergyInfo(timeSinceBoot, stackState, txDuration, rxDuration,
+                    scanDuration, idleDuration);
+        }
+    }
+
+    // See WifiActivityEnergyInfo
+    private long calculateEnergyMicroJoules(
+            long txDurationMillis, long rxDurationMillis, long idleDurationMillis) {
+        PowerProfile powerProfile = mStatsRule.getPowerProfile();
+        final double idleCurrent = powerProfile.getAveragePower(
+                PowerProfile.POWER_WIFI_CONTROLLER_IDLE);
+        final double rxCurrent = powerProfile.getAveragePower(
+                PowerProfile.POWER_WIFI_CONTROLLER_RX);
+        final double txCurrent = powerProfile.getAveragePower(
+                PowerProfile.POWER_WIFI_CONTROLLER_TX);
+        final double voltage = powerProfile.getAveragePower(
+                PowerProfile.POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
+
+        return (long) ((txDurationMillis * txCurrent
+                + rxDurationMillis * rxCurrent
+                + idleDurationMillis * idleCurrent)
+                * voltage);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
index ef80b59..f86cb7b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
@@ -21,9 +21,12 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -31,10 +34,14 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.Manifest;
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.AccessibilityTrace;
+import android.accessibilityservice.BrailleDisplayController;
 import android.accessibilityservice.GestureDescription;
 import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IBrailleDisplayConnection;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -43,6 +50,9 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.hardware.display.DisplayManager;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -62,7 +72,9 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
@@ -94,18 +106,30 @@
     AccessibilityServiceInfo mServiceInfo;
     @Mock ResolveInfo mMockResolveInfo;
     @Mock AccessibilitySecurityPolicy mMockSecurityPolicy;
-    @Mock AccessibilityWindowManager mMockA11yWindowManager;
-    @Mock ActivityTaskManagerInternal mMockActivityTaskManagerInternal;
-    @Mock AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
-    @Mock AccessibilityTrace mMockA11yTrace;
-    @Mock WindowManagerInternal mMockWindowManagerInternal;
-    @Mock SystemActionPerformer mMockSystemActionPerformer;
-    @Mock KeyEventDispatcher mMockKeyEventDispatcher;
+    @Mock
+    AccessibilityWindowManager mMockA11yWindowManager;
+    @Mock
+    ActivityTaskManagerInternal mMockActivityTaskManagerInternal;
+    @Mock
+    AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
+    @Mock
+    AccessibilityTrace mMockA11yTrace;
+    @Mock
+    WindowManagerInternal mMockWindowManagerInternal;
+    @Mock
+    SystemActionPerformer mMockSystemActionPerformer;
+    @Mock
+    KeyEventDispatcher mMockKeyEventDispatcher;
     @Mock
     MagnificationProcessor mMockMagnificationProcessor;
-    @Mock IBinder mMockIBinder;
-    @Mock IAccessibilityServiceClient mMockServiceClient;
-    @Mock MotionEventInjector mMockMotionEventInjector;
+    @Mock
+    IBinder mMockIBinder;
+    @Mock
+    IAccessibilityServiceClient mMockServiceClient;
+    @Mock
+    IBrailleDisplayController mMockBrailleDisplayController;
+    @Mock
+    MotionEventInjector mMockMotionEventInjector;
 
     MessageCapturingHandler mHandler = new MessageCapturingHandler(null);
 
@@ -134,6 +158,7 @@
                 mMockWindowManagerInternal, mMockSystemActionPerformer,
                 mMockA11yWindowManager, mMockActivityTaskManagerInternal);
         when(mMockSecurityPolicy.canPerformGestures(mConnection)).thenReturn(true);
+        when(mMockSecurityPolicy.checkAccessibilityAccess(mConnection)).thenReturn(true);
     }
 
     @After
@@ -291,6 +316,119 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay() throws Exception {
+        final String macAddress = "00:11:22:33:AA:BB";
+        final byte[] descriptor = {0x05, 0x41};
+        Bundle bd = new Bundle();
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null");
+        bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor);
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, macAddress);
+        bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, true);
+        mConnection.setTestBrailleDisplayData(List.of(bd));
+
+        mConnection.connectBluetoothBrailleDisplay(macAddress, mMockBrailleDisplayController);
+
+        ArgumentCaptor<IBrailleDisplayConnection> connection =
+                ArgumentCaptor.forClass(IBrailleDisplayConnection.class);
+        verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor));
+        // Cleanup the connection.
+        connection.getValue().disconnect();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForMissingBluetoothConnectPermission() {
+        doThrow(SecurityException.class).when(mMockContext)
+                .enforceCallingPermission(eq(Manifest.permission.BLUETOOTH_CONNECT), any());
+
+        assertThrows(SecurityException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay("unused",
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForNullMacAddress() {
+        assertThrows(NullPointerException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay(null,
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectBluetoothBrailleDisplay_throwsForMisformattedMacAddress() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mConnection.connectBluetoothBrailleDisplay("12:34",
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay() throws Exception {
+        final String serialNumber = "myUsbDevice";
+        final byte[] descriptor = {0x05, 0x41};
+        Bundle bd = new Bundle();
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null");
+        bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor);
+        bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, serialNumber);
+        bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, false);
+        mConnection.setTestBrailleDisplayData(List.of(bd));
+        UsbDevice usbDevice = Mockito.mock(UsbDevice.class);
+        when(usbDevice.getSerialNumber()).thenReturn(serialNumber);
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(eq(usbDevice), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(true);
+
+        mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController);
+
+        ArgumentCaptor<IBrailleDisplayConnection> connection =
+                ArgumentCaptor.forClass(IBrailleDisplayConnection.class);
+        verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor));
+        // Cleanup the connection.
+        connection.getValue().disconnect();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_throwsForMissingUsbPermission() {
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(false);
+
+        assertThrows(SecurityException.class,
+                () -> mConnection.connectUsbBrailleDisplay(Mockito.mock(UsbDevice.class),
+                        mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_throwsForNullDevice() {
+        assertThrows(NullPointerException.class,
+                () -> mConnection.connectUsbBrailleDisplay(null, mMockBrailleDisplayController));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+    public void connectUsbBrailleDisplay_callsOnConnectionFailedForEmptySerialNumber()
+            throws Exception {
+        UsbManager usbManager = Mockito.mock(UsbManager.class);
+        when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+        when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()),
+                anyInt(), anyInt())).thenReturn(true);
+        UsbDevice usbDevice = Mockito.mock(UsbDevice.class);
+        when(usbDevice.getSerialNumber()).thenReturn("");
+
+        mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController);
+
+        verify(mMockBrailleDisplayController).onConnectionFailed(
+                BrailleDisplayController.BrailleDisplayCallback
+                        .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_RESETTABLE_DYNAMIC_PROPERTIES)
     public void binderDied_resetA11yServiceInfo() {
         final int flag = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
new file mode 100644
index 0000000..7c278ce
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.accessibility;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.accessibilityservice.BrailleDisplayController;
+import android.os.Bundle;
+import android.testing.DexmakerShareClassLoaderRule;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Tests for internal details of {@link BrailleDisplayConnection}.
+ *
+ * <p>Prefer adding new tests in CTS where possible.
+ */
+public class BrailleDisplayConnectionTest {
+    private static final Path NULL_PATH = Path.of("/dev/null");
+
+    private BrailleDisplayConnection mBrailleDisplayConnection;
+    @Mock
+    private BrailleDisplayConnection.NativeInterface mNativeInterface;
+    @Mock
+    private AccessibilityServiceConnection mServiceConnection;
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    // To mock package-private class
+    @Rule
+    public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+            new DexmakerShareClassLoaderRule();
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mBrailleDisplayConnection = new BrailleDisplayConnection(new Object(), mServiceConnection);
+    }
+
+    @Test
+    public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
+        int descriptorSize = 4;
+        byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
+        when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
+        when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
+    }
+
+    @Test
+    public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
+        when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
+    }
+
+    @Test
+    public void defaultNativeScanner_getUniqueId_returnsUniq() {
+        String macAddress = "12:34:56:78";
+        when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
+    }
+
+    @Test
+    public void defaultNativeScanner_getDeviceBusType_busUsb() {
+        when(mNativeInterface.getHidrawBusType(anyInt()))
+                .thenReturn(BrailleDisplayConnection.BUS_USB);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceBusType(NULL_PATH))
+                .isEqualTo(BrailleDisplayConnection.BUS_USB);
+    }
+
+    @Test
+    public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
+        when(mNativeInterface.getHidrawBusType(anyInt()))
+                .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+        assertThat(scanner.getDeviceBusType(NULL_PATH))
+                .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
+    }
+
+    // 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.
+    @Test
+    public void setTestData_scannerReturnsTestData() {
+        Bundle bd1 = new Bundle(), bd2 = new Bundle();
+
+        Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
+        bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString());
+        bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString());
+        byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
+        bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
+        bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
+        String uniq1 = "uniq1", uniq2 = "uniq2";
+        bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
+        bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
+        int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH;
+        bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+                bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
+        bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+                bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
+
+        BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
+
+        expect.that(scanner.getHidrawNodePaths()).containsExactly(path1, path2);
+        expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
+        expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
+        expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
+        expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
+        expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
+        expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 344a4b0..4dded1d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -16,6 +16,7 @@
 package com.android.server.notification;
 
 import static android.content.Context.DEVICE_POLICY_SERVICE;
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
 import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
 import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
 import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
@@ -62,6 +63,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -1983,6 +1985,22 @@
                 new ComponentName("pkg1", "cmp1"))).isFalse();
     }
 
+    @Test
+    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public void testManagedServiceInfoIsSystemUi() {
+        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+                APPROVAL_BY_COMPONENT);
+
+        ManagedServices.ManagedServiceInfo service0 = service.new ManagedServiceInfo(
+                mock(IInterface.class), ComponentName.unflattenFromString("a/a"), 0, false,
+                mock(ServiceConnection.class), 26, 34);
+
+        service0.isSystemUi = true;
+        assertThat(service0.isSystemUi()).isTrue();
+        service0.isSystemUi = false;
+        assertThat(service0.isSystemUi()).isFalse();
+    }
+
     private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
             ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
             throws RemoteException {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 96ffec1..046e057 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -2545,6 +2545,17 @@
         assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
         assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
 
+        // Checks that a post update is sent.
+        verify(mWorkerHandler, times(1))
+                .post(any(NotificationManagerService.PostNotificationRunnable.class));
+        ArgumentCaptor<NotificationRecord> captor =
+                ArgumentCaptor.forClass(NotificationRecord.class);
+        verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+                anyBoolean());
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+
         mSetFlagsRule.disableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
         mBinderService.cancelNotificationWithTag(PKG, PKG, sbn.getTag(), sbn.getId(),
                 sbn.getUserId());
@@ -2577,6 +2588,17 @@
         StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG);
         assertThat(notifs.length).isEqualTo(1);
         assertThat(notifs[0].getId()).isEqualTo(1);
+
+        // Checks that a post update is sent.
+        verify(mWorkerHandler, times(1))
+                .post(any(NotificationManagerService.PostNotificationRunnable.class));
+        ArgumentCaptor<NotificationRecord> captor =
+                ArgumentCaptor.forClass(NotificationRecord.class);
+        verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+                anyBoolean());
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
     }
 
     @Test
@@ -2985,18 +3007,29 @@
     public void testCancelNotificationsFromListener_clearAll_NoClearLifetimeExt()
             throws Exception {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
-
         final NotificationRecord notif = generateNotificationRecord(
                 mTestNotificationChannel, 1, null, false);
-        notif.getNotification().flags = FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        notif.getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
         mService.addNotification(notif);
-
+        verify(mWorkerHandler, times(0))
+                .post(any(NotificationManagerService.PostNotificationRunnable.class));
         mService.getBinderService().cancelNotificationsFromListener(null, null);
         waitForIdle();
-
+        // Notification not cancelled.
         StatusBarNotification[] notifs =
                 mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
         assertThat(notifs.length).isEqualTo(1);
+
+        // Checks that a post update is sent.
+        verify(mWorkerHandler, times(1))
+                .post(any(NotificationManagerService.PostNotificationRunnable.class));
+        ArgumentCaptor<NotificationRecord> captor =
+                ArgumentCaptor.forClass(NotificationRecord.class);
+        verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+                anyBoolean());
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
     }
 
     @Test
@@ -3217,6 +3250,17 @@
         StatusBarNotification[] notifs =
                 mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
         assertEquals(1, notifs.length);
+
+        // Checks that a post update is sent.
+        verify(mWorkerHandler, times(1))
+                .post(any(NotificationManagerService.PostNotificationRunnable.class));
+        ArgumentCaptor<NotificationRecord> captor =
+                ArgumentCaptor.forClass(NotificationRecord.class);
+        verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+                anyBoolean());
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
     }
 
     @Test
@@ -5659,6 +5703,17 @@
         StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
         assertThat(notifsAfter.length).isEqualTo(1);
         assertThat(mService.getNotificationRecord(notif.getKey())).isEqualTo(notif);
+
+        // Checks that a post update is sent.
+        verify(mWorkerHandler, times(1))
+                .post(any(NotificationManagerService.PostNotificationRunnable.class));
+        ArgumentCaptor<NotificationRecord> captor =
+                ArgumentCaptor.forClass(NotificationRecord.class);
+        verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+                anyBoolean());
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+                FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
     }
 
     @Test
diff --git a/services/usb/Android.bp b/services/usb/Android.bp
index 3a0a6ab..e8ffe54 100644
--- a/services/usb/Android.bp
+++ b/services/usb/Android.bp
@@ -34,8 +34,20 @@
         "android.hardware.usb-V1.2-java",
         "android.hardware.usb-V1.3-java",
         "android.hardware.usb-V3-java",
+        "usb_flags_lib",
     ],
     lint: {
         baseline_filename: "lint-baseline.xml",
     },
 }
+
+aconfig_declarations {
+    name: "usb_flags",
+    package: "com.android.server.usb.flags",
+    srcs: ["**/usb_flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "usb_flags_lib",
+    aconfig_declarations: "usb_flags",
+}
diff --git a/services/usb/java/com/android/server/usb/UsbHandlerManager.java b/services/usb/java/com/android/server/usb/UsbHandlerManager.java
index f311274..d83ff1f 100644
--- a/services/usb/java/com/android/server/usb/UsbHandlerManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHandlerManager.java
@@ -37,7 +37,7 @@
  *
  * @hide
  */
-class UsbHandlerManager {
+public class UsbHandlerManager {
     private static final String LOG_TAG = UsbHandlerManager.class.getSimpleName();
 
     private final Context mContext;
diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
index f916660..2ff21ad 100644
--- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -62,6 +63,7 @@
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.usb.flags.Flags;
 import com.android.server.utils.EventLogger;
 
 import libcore.io.IoUtils;
@@ -80,8 +82,20 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
-class UsbProfileGroupSettingsManager {
+public class UsbProfileGroupSettingsManager {
+    /**
+     * &lt;application&gt; level property that an app can specify to restrict any overlaying of
+     * activities when usb device is attached.
+     *
+     *
+     * <p>This should only be set by privileged apps having {@link Manifest.permission#MANAGE_USB}
+     * permission.
+     * @hide
+     */
+    public static final String PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES =
+            "android.app.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES";
     private static final String TAG = UsbProfileGroupSettingsManager.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -101,6 +115,8 @@
 
     private final PackageManager mPackageManager;
 
+    private final ActivityManager mActivityManager;
+
     private final UserManager mUserManager;
     private final @NonNull UsbSettingsManager mSettingsManager;
 
@@ -224,7 +240,7 @@
      * @param settingsManager The settings manager of the service
      * @param usbResolveActivityManager The resovle activity manager of the service
      */
-    UsbProfileGroupSettingsManager(@NonNull Context context, @NonNull UserHandle user,
+    public UsbProfileGroupSettingsManager(@NonNull Context context, @NonNull UserHandle user,
             @NonNull UsbSettingsManager settingsManager,
             @NonNull UsbHandlerManager usbResolveActivityManager) {
         if (DEBUG) Slog.v(TAG, "Creating settings for " + user);
@@ -238,6 +254,7 @@
 
         mContext = context;
         mPackageManager = context.getPackageManager();
+        mActivityManager = context.getSystemService(ActivityManager.class);
         mSettingsManager = settingsManager;
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
 
@@ -895,7 +912,10 @@
         // Send broadcast to running activities with registered intent
         mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
 
-        resolveActivity(intent, device, true /* showMtpNotification */);
+        //resolving activities only if there is no foreground activity restricting it.
+        if (!shouldRestrictOverlayActivities()) {
+            resolveActivity(intent, device, true /* showMtpNotification */);
+        }
     }
 
     private void resolveActivity(Intent intent, UsbDevice device, boolean showMtpNotification) {
@@ -918,6 +938,63 @@
         resolveActivity(intent, matches, defaultActivity, device, null);
     }
 
+    /**
+     * @return true if any application in foreground have set restrict_usb_overlay_activities as
+     * true in manifest file. The application needs to have MANAGE_USB permission.
+     */
+    private boolean shouldRestrictOverlayActivities() {
+
+        if (!Flags.allowRestrictionOfOverlayActivities()) return false;
+
+        List<ActivityManager.RunningAppProcessInfo> appProcessInfos = mActivityManager
+                .getRunningAppProcesses();
+
+        List<String> filteredAppProcessInfos = new ArrayList<>();
+        boolean shouldRestrictOverlayActivities;
+
+        //filtering out applications in foreground.
+        for (ActivityManager.RunningAppProcessInfo processInfo : appProcessInfos) {
+            if (processInfo.importance <= ActivityManager
+                    .RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+                filteredAppProcessInfos.addAll(List.of(processInfo.pkgList));
+            }
+        }
+
+        if (DEBUG) Slog.d(TAG, "packages in foreground : " + filteredAppProcessInfos);
+
+        List<String> packagesHoldingManageUsbPermission =
+                mPackageManager.getPackagesHoldingPermissions(
+                        new String[]{android.Manifest.permission.MANAGE_USB},
+                        PackageManager.MATCH_SYSTEM_ONLY).stream()
+                        .map(packageInfo -> packageInfo.packageName).collect(Collectors.toList());
+
+        //retaining only packages that hold the required permission
+        filteredAppProcessInfos.retainAll(packagesHoldingManageUsbPermission);
+
+        if (DEBUG) {
+            Slog.d(TAG, "packages in foreground with required permission : "
+                    + filteredAppProcessInfos);
+        }
+
+        shouldRestrictOverlayActivities = filteredAppProcessInfos.stream().anyMatch(pkg -> {
+            try {
+                return mPackageManager.getProperty(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES, pkg)
+                        .getBoolean();
+            } catch (NameNotFoundException e) {
+                if (DEBUG) {
+                    Slog.d(TAG, "property PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES "
+                            + "not present for " + pkg);
+                }
+                return false;
+            }
+        });
+
+        if (shouldRestrictOverlayActivities) {
+            Slog.d(TAG, "restricting starting of usb overlay activities");
+        }
+        return shouldRestrictOverlayActivities;
+    }
+
     public void deviceAttachedForFixedHandler(UsbDevice device, ComponentName component) {
         final Intent intent = createDeviceAttachedIntent(device);
 
diff --git a/services/usb/java/com/android/server/usb/UsbSettingsManager.java b/services/usb/java/com/android/server/usb/UsbSettingsManager.java
index 8e53ff4..0b854a8 100644
--- a/services/usb/java/com/android/server/usb/UsbSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbSettingsManager.java
@@ -33,7 +33,7 @@
 /**
  * Maintains all {@link UsbUserSettingsManager} for all users.
  */
-class UsbSettingsManager {
+public class UsbSettingsManager {
     private static final String LOG_TAG = UsbSettingsManager.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -70,7 +70,7 @@
      *
      * @return The settings for the user
      */
-    @NonNull UsbUserSettingsManager getSettingsForUser(@UserIdInt int userId) {
+    public @NonNull UsbUserSettingsManager getSettingsForUser(@UserIdInt int userId) {
         synchronized (mSettingsByUser) {
             UsbUserSettingsManager settings = mSettingsByUser.get(userId);
             if (settings == null) {
diff --git a/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java b/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
index c2b8d01..be729c5 100644
--- a/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
@@ -49,7 +49,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-class UsbUserSettingsManager {
+public class UsbUserSettingsManager {
     private static final String TAG = UsbUserSettingsManager.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -81,7 +81,7 @@
      *
      * @return The resolve infos of the activities that can handle the intent
      */
-    List<ResolveInfo> queryIntentActivities(@NonNull Intent intent) {
+    public List<ResolveInfo> queryIntentActivities(@NonNull Intent intent) {
         return mPackageManager.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA,
                 mUser.getIdentifier());
     }
diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
new file mode 100644
index 0000000..ea6e502
--- /dev/null
+++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.usb.flags"
+
+flag {
+    name: "allow_restriction_of_overlay_activities"
+    namespace: "usb"
+    description: "This flag controls the restriction of usb overlay activities"
+    bug: "307231174"
+}
diff --git a/telephony/java/android/telephony/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java
index 3c11da5..4ff9712 100644
--- a/telephony/java/android/telephony/DomainSelectionService.java
+++ b/telephony/java/android/telephony/DomainSelectionService.java
@@ -831,7 +831,7 @@
             @NonNull String tag, @NonNull String errorLogName) {
         try {
             CompletableFuture.runAsync(
-                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor).join();
+                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor);
         } catch (CancellationException | CompletionException e) {
             Rlog.w(tag, "Binder - " + errorLogName + " exception: " + e.getMessage());
         }
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index 711be02..9441fb5 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -501,4 +501,22 @@
      *   SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
      */
      void stopSendingNtnSignalStrength(in IIntegerConsumer resultCallback);
+
+     /**
+      * Abort all outgoing satellite datagrams which vendor service has received from Telephony
+      * framework.
+      *
+      * This API helps modem to be in sync with framework when framework times out on sending
+      * datagrams.
+      *
+      * @param resultCallback The callback to receive the error code result of the operation.
+      *
+      * Valid result codes returned:
+      *   SatelliteResult:SATELLITE_RESULT_SUCCESS
+      *   SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
+      *   SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
+      *   SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
+      *   SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
+      */
+     void abortSendingSatelliteDatagrams(in IIntegerConsumer resultCallback);
 }
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index abacd15..f17ff17 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -265,6 +265,14 @@
                     "stopSendingNtnSignalStrength");
         }
 
+        @Override
+        public void abortSendingSatelliteDatagrams(IIntegerConsumer resultCallback)
+                throws RemoteException {
+            executeMethodAsync(
+                    () -> SatelliteImplBase.this.abortSendingSatelliteDatagrams(resultCallback),
+                    "abortSendingSatelliteDatagrams");
+        }
+
         // Call the methods with a clean calling identity on the executor and wait indefinitely for
         // the future to return.
         private void executeMethodAsync(Runnable r, String errorLogName) throws RemoteException {
@@ -783,4 +791,13 @@
     public void stopSendingNtnSignalStrength(@NonNull IIntegerConsumer resultCallback){
         // stub implementation
     }
+
+    /**
+     * Requests to abort sending satellite datagrams
+     *
+     * @param resultCallback The {@link SatelliteError} result of the operation.
+     */
+    public void abortSendingSatelliteDatagrams(@NonNull IIntegerConsumer resultCallback){
+        // stub implementation
+    }
 }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index bd47b1f..ff2ee27 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3046,13 +3046,24 @@
     boolean setSatellitePointingUiClassName(in String packageName, in String className);
 
     /**
-     * This API can be used by only CTS to update the timeout duration in milliseconds whether
-     * the device is aligned with the satellite for demo mode
+     * This API can be used by only CTS to override the timeout durations used by the
+     * DatagramController module.
      *
      * @param timeoutMillis The timeout duration in millisecond.
      * @return {@code true} if the timeout duration is set successfully, {@code false} otherwise.
      */
-    boolean setSatelliteDeviceAlignedTimeoutDuration(long timeoutMillis);
+    boolean setDatagramControllerTimeoutDuration(
+            boolean reset, int timeoutType, long timeoutMillis);
+
+    /**
+     * This API can be used by only CTS to override the timeout durations used by the
+     * SatelliteController module.
+     *
+     * @param timeoutMillis The timeout duration in millisecond.
+     * @return {@code true} if the timeout duration is set successfully, {@code false} otherwise.
+     */
+    boolean setSatelliteControllerTimeoutDuration(
+            boolean reset, int timeoutType, long timeoutMillis);
 
     /**
      * This API can be used in only testing to override connectivity status in monitoring emergency
diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp
index c02d8e9..70c7dad 100644
--- a/tests/UsbManagerTests/Android.bp
+++ b/tests/UsbManagerTests/Android.bp
@@ -29,12 +29,17 @@
     static_libs: [
         "frameworks-base-testutils",
         "androidx.test.rules",
-        "mockito-target-inline-minus-junit4",
+        "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
         "truth",
         "UsbManagerTestLib",
     ],
-    jni_libs: ["libdexmakerjvmtiagent"],
+    jni_libs: [
+        // Required for ExtendedMockito
+        "libdexmakerjvmtiagent",
+        "libmultiplejvmtiagentsinterferenceagent",
+        "libstaticjvmtiagent",
+    ],
     certificate: "platform",
     platform_apis: true,
     test_suites: ["device-tests"],
diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
new file mode 100644
index 0000000..4780d8a
--- /dev/null
+++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.usbtest;
+
+import static com.android.server.usb.UsbProfileGroupSettingsManager.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.Property;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.hardware.usb.UsbDevice;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.usb.UsbHandlerManager;
+import com.android.server.usb.UsbProfileGroupSettingsManager;
+import com.android.server.usb.UsbSettingsManager;
+import com.android.server.usb.UsbUserSettingsManager;
+import com.android.server.usb.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link com.android.server.usb.UsbProfileGroupSettingsManager}.
+ * Note: MUST claim MANAGE_USB permission in Manifest
+ */
+@RunWith(AndroidJUnit4.class)
+public class UsbProfileGroupSettingsManagerTest {
+
+    private static final String TEST_PACKAGE_NAME = "testPkg";
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private ActivityManager mActivityManager;
+    @Mock
+    private UserHandle mUserHandle;
+    @Mock
+    private UsbSettingsManager mUsbSettingsManager;
+    @Mock
+    private UsbHandlerManager mUsbHandlerManager;
+    @Mock
+    private UserManager mUserManager;
+    @Mock
+    private UsbUserSettingsManager mUsbUserSettingsManager;
+    @Mock private Property mProperty;
+    private ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo;
+    private PackageInfo mPackageInfo;
+    private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager;
+    private MockitoSession mStaticMockSession;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mRunningAppProcessInfo = new ActivityManager.RunningAppProcessInfo();
+        mRunningAppProcessInfo.pkgList = new String[]{TEST_PACKAGE_NAME};
+        mPackageInfo = new PackageInfo();
+        mPackageInfo.packageName = TEST_PACKAGE_NAME;
+        mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
+
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
+        when(mContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+        when(mContext.createPackageContextAsUser(anyString(), anyInt(), any(UserHandle.class)))
+                .thenReturn(mContext);
+        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+
+        mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(mContext, mUserHandle,
+                mUsbSettingsManager, mUsbHandlerManager);
+
+        mStaticMockSession = ExtendedMockito.mockitoSession()
+                .mockStatic(Flags.class)
+                .strictness(Strictness.WARN)
+                .startMocking();
+
+        when(mPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mPackageInfo);
+        when(mPackageManager.getProperty(eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES),
+                eq(TEST_PACKAGE_NAME))).thenReturn(mProperty);
+        when(mUserManager.getEnabledProfiles(anyInt()))
+                .thenReturn(List.of(Mockito.mock(UserInfo.class)));
+        when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mStaticMockSession.finishMocking();
+    }
+
+    @Test
+    public void testDeviceAttached_flagTrueWithoutForegroundActivity_resolveActivityCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(new ArrayList<>());
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_noForegroundActivityWithUsbPermission_resolveActivityCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(new ArrayList<>());
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+        when(mProperty.getBoolean()).thenReturn(true);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager, times(0))
+                .queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_foregroundActivityWithoutManifestField_resolveActivityCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+        when(mProperty.getBoolean()).thenReturn(false);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_flagFalseForegroundActivity_resolveActivityCalled() {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(false);
+        when(mProperty.getBoolean()).thenReturn(true);
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[]{android.Manifest.permission.MANAGE_USB},
+                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+        UsbDevice device = Mockito.mock(UsbDevice.class);
+        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java
index 631fc02..eba9910 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java
@@ -17,6 +17,7 @@
 
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
+import android.os.Parcel;
 import android.util.Base64;
 
 import java.text.DecimalFormat;
@@ -31,6 +32,7 @@
     private static final HashMap<Long, CursorWindow_host> sInstances = new HashMap<>();
     private static long sNextId = 1;
 
+    private String mName;
     private int mColumnNum;
     private static class Row {
         String[] fields;
@@ -41,6 +43,7 @@
 
     public static long nativeCreate(String name, int cursorWindowSize) {
         CursorWindow_host instance = new CursorWindow_host();
+        instance.mName = name;
         long instanceId = sNextId++;
         sInstances.put(instanceId, instance);
         return instanceId;
@@ -50,6 +53,10 @@
         sInstances.remove(windowPtr);
     }
 
+    public static String nativeGetName(long windowPtr) {
+        return sInstances.get(windowPtr).mName;
+    }
+
     public static boolean nativeSetNumColumns(long windowPtr, int columnNum) {
         sInstances.get(windowPtr).mColumnNum = columnNum;
         return true;
@@ -156,4 +163,30 @@
                 return null;
         }
     }
+
+    public static void nativeWriteToParcel(long windowPtr, Parcel parcel) {
+        CursorWindow_host window = sInstances.get(windowPtr);
+        parcel.writeString(window.mName);
+        parcel.writeInt(window.mColumnNum);
+        parcel.writeInt(window.mRows.size());
+        for (int row = 0; row < window.mRows.size(); row++) {
+            parcel.writeStringArray(window.mRows.get(row).fields);
+            parcel.writeIntArray(window.mRows.get(row).types);
+        }
+    }
+
+    public static long nativeCreateFromParcel(Parcel parcel) {
+        long windowPtr = nativeCreate(null, 0);
+        CursorWindow_host window = sInstances.get(windowPtr);
+        window.mName = parcel.readString();
+        window.mColumnNum = parcel.readInt();
+        int rowCount = parcel.readInt();
+        for (int row = 0; row < rowCount; row++) {
+            Row r = new Row();
+            r.fields = parcel.createStringArray();
+            r.types = parcel.createIntArray();
+            window.mRows.add(r);
+        }
+        return windowPtr;
+    }
 }
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java
index a135623..4d39d88 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java
@@ -143,6 +143,16 @@
             updateValue(values, timestampMs);
         }
 
+        public void addCounts(long[] delta) {
+            if (!mEnabled) {
+                return;
+            }
+
+            for (int i = 0; i < mArrayLength; i++) {
+                mStates[mCurrentState].mCounter[i] += delta[i];
+            }
+        }
+
         public void getValues(long[] values, int state) {
             System.arraycopy(mStates[state].mCounter, 0, values, 0, mArrayLength);
         }
@@ -331,6 +341,10 @@
                 LongArrayContainer_host.getInstance(containerInstanceId), timestampMs);
     }
 
+    public static void native_addCounts(long instanceId, long containerInstanceId) {
+        getInstance(instanceId).addCounts(LongArrayContainer_host.getInstance(containerInstanceId));
+    }
+
     public static void native_getCounts(long instanceId, long containerInstanceId, int state) {
         getInstance(instanceId).getValues(LongArrayContainer_host.getInstance(containerInstanceId),
                 state);
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java
new file mode 100644
index 0000000..a5d0fc6
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java
@@ -0,0 +1,263 @@
+/*
+ * 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.hoststubgen.nativesubstitution;
+
+import android.os.BadParcelableException;
+import android.os.Parcel;
+
+import java.util.HashMap;
+
+/**
+ * Native implementation substitutions for the LongMultiStateCounter class.
+ */
+public class LongMultiStateCounter_host {
+
+    /**
+     * A reimplementation of {@link com.android.internal.os.LongMultiStateCounter}, only in
+     * Java instead of native.  The majority of the code (in C++) can be found in
+     * /frameworks/native/libs/battery/MultiStateCounter.h
+     */
+    private static class LongMultiStateCounterRavenwood {
+        private final int mStateCount;
+        private int mCurrentState;
+        private long mLastStateChangeTimestampMs = -1;
+        private long mLastUpdateTimestampMs = -1;
+        private boolean mEnabled = true;
+
+        private static class State {
+            private long mTimeInStateSinceUpdate;
+            private long mCounter;
+        }
+
+        private final State[] mStates;
+        private long mValue;
+
+        LongMultiStateCounterRavenwood(int stateCount) {
+            mStateCount = stateCount;
+            mStates = new State[stateCount];
+            for (int i = 0; i < mStateCount; i++) {
+                mStates[i] = new State();
+            }
+        }
+
+        public void setEnabled(boolean enabled, long timestampMs) {
+            if (enabled == mEnabled) {
+                return;
+            }
+
+            if (!enabled) {
+                setState(mCurrentState, timestampMs);
+                mEnabled = false;
+            } else {
+                if (timestampMs < mLastUpdateTimestampMs) {
+                    timestampMs = mLastUpdateTimestampMs;
+                }
+
+                if (mLastStateChangeTimestampMs >= 0) {
+                    mLastStateChangeTimestampMs = timestampMs;
+                }
+                mEnabled = true;
+            }
+        }
+
+        public void setState(int state, long timestampMs) {
+            if (mEnabled && mLastStateChangeTimestampMs >= 0 && mLastUpdateTimestampMs >= 0) {
+                if (timestampMs < mLastUpdateTimestampMs) {
+                    timestampMs = mLastUpdateTimestampMs;
+                }
+
+                if (timestampMs >= mLastStateChangeTimestampMs) {
+                    mStates[mCurrentState].mTimeInStateSinceUpdate +=
+                            timestampMs - mLastStateChangeTimestampMs;
+                } else {
+                    for (int i = 0; i < mStateCount; i++) {
+                        mStates[i].mTimeInStateSinceUpdate = 0;
+                    }
+                }
+            }
+            mCurrentState = state;
+            mLastStateChangeTimestampMs = timestampMs;
+        }
+
+        public long updateValue(long value, long timestampMs) {
+            long returnValue = 0;
+            if (mEnabled || mLastUpdateTimestampMs < mLastStateChangeTimestampMs) {
+                if (timestampMs < mLastStateChangeTimestampMs) {
+                    timestampMs = mLastStateChangeTimestampMs;
+                }
+
+                setState(mCurrentState, timestampMs);
+
+                if (mLastUpdateTimestampMs >= 0) {
+                    if (timestampMs > mLastUpdateTimestampMs) {
+                        long delta = value - mValue;
+                        if (delta >= 0) {
+                            returnValue = delta;
+                            long timeSinceUpdate = timestampMs - mLastUpdateTimestampMs;
+                            for (int i = 0; i < mStateCount; i++) {
+                                long timeInState = mStates[i].mTimeInStateSinceUpdate;
+                                if (timeInState > 0) {
+                                    mStates[i].mCounter += delta * timeInState / timeSinceUpdate;
+                                    mStates[i].mTimeInStateSinceUpdate = 0;
+                                }
+                            }
+                        } else {
+                            for (int i = 0; i < mStateCount; i++) {
+                                mStates[i].mTimeInStateSinceUpdate = 0;
+                            }
+                        }
+                    } else if (timestampMs < mLastUpdateTimestampMs) {
+                        for (int i = 0; i < mStateCount; i++) {
+                            mStates[i].mTimeInStateSinceUpdate = 0;
+                        }
+                    }
+                }
+            }
+            mValue = value;
+            mLastUpdateTimestampMs = timestampMs;
+            return returnValue;
+        }
+
+        public void incrementValue(long count, long timestampMs) {
+            updateValue(mValue + count, timestampMs);
+        }
+
+        public long getValue(int state) {
+            return mStates[state].mCounter;
+        }
+
+        public void reset() {
+            mLastStateChangeTimestampMs = -1;
+            mLastUpdateTimestampMs = -1;
+            for (int i = 0; i < mStateCount; i++) {
+                mStates[i].mTimeInStateSinceUpdate = 0;
+                mStates[i].mCounter = 0;
+            }
+        }
+
+        public void writeToParcel(Parcel parcel) {
+            parcel.writeInt(mStateCount);
+            for (int i = 0; i < mStateCount; i++) {
+                parcel.writeLong(mStates[i].mCounter);
+            }
+        }
+
+        public void initFromParcel(Parcel parcel) {
+            try {
+                for (int i = 0; i < mStateCount; i++) {
+                    mStates[i].mCounter = parcel.readLong();
+                }
+            } catch (Exception e) {
+                throw new BadParcelableException(e);
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("[");
+            for (int state = 0; state < mStateCount; state++) {
+                if (state != 0) {
+                    sb.append(", ");
+                }
+                sb.append(state).append(": ").append(mStates[state].mCounter);
+            }
+            sb.append("]");
+            if (mLastUpdateTimestampMs >= 0) {
+                sb.append(" updated: ").append(mLastUpdateTimestampMs);
+            }
+            if (mLastStateChangeTimestampMs >= 0) {
+                sb.append(" currentState: ").append(mCurrentState);
+                if (mLastStateChangeTimestampMs > mLastUpdateTimestampMs) {
+                    sb.append(" stateChanged: ").append(mLastStateChangeTimestampMs);
+                }
+            } else {
+                sb.append(" currentState: none");
+            }
+            return sb.toString();
+        }
+    }
+
+    private static final HashMap<Long, LongMultiStateCounterRavenwood> sInstances =
+            new HashMap<>();
+    private static long sNextId = 1;
+
+    public static long native_init(int stateCount) {
+        LongMultiStateCounterRavenwood instance = new LongMultiStateCounterRavenwood(stateCount);
+        long instanceId = sNextId++;
+        sInstances.put(instanceId, instance);
+        return instanceId;
+    }
+
+    private static LongMultiStateCounterRavenwood getInstance(long instanceId) {
+        return sInstances.get(instanceId);
+    }
+
+    public static void native_setEnabled(long instanceId, boolean enabled,
+            long timestampMs) {
+        getInstance(instanceId).setEnabled(enabled, timestampMs);
+    }
+
+    public static int native_getStateCount(long instanceId) {
+        return getInstance(instanceId).mStateCount;
+    }
+
+    public static long native_updateValue(long instanceId, long value, long timestampMs) {
+        return getInstance(instanceId).updateValue(value, timestampMs);
+    }
+
+    public static void native_setState(long instanceId, int state, long timestampMs) {
+        getInstance(instanceId).setState(state, timestampMs);
+    }
+
+    public static void native_incrementValue(long instanceId, long count, long timestampMs) {
+        getInstance(instanceId).incrementValue(count, timestampMs);
+    }
+
+    public static long native_getCount(long instanceId, int state) {
+        return getInstance(instanceId).getValue(state);
+    }
+
+    public static void native_reset(long instanceId) {
+        getInstance(instanceId).reset();
+    }
+
+    public static void native_writeToParcel(long instanceId, Parcel parcel, int flags) {
+        getInstance(instanceId).writeToParcel(parcel);
+    }
+
+    public static long native_initFromParcel(Parcel parcel) {
+        int stateCount = parcel.readInt();
+        if (stateCount < 0 || stateCount > 0xEFFF) {
+            throw new BadParcelableException("stateCount out of range");
+        }
+        // LongMultiStateCounter.cpp uses AParcel, which throws on out-of-data.
+        if (parcel.dataPosition() >= parcel.dataSize()) {
+            throw new RuntimeException("Bad parcel");
+        }
+        long instanceId = native_init(stateCount);
+        getInstance(instanceId).initFromParcel(parcel);
+        if (parcel.dataPosition() > parcel.dataSize()) {
+            throw new RuntimeException("Bad parcel");
+        }
+        return instanceId;
+    }
+
+    public static String native_toString(long instanceId) {
+        return getInstance(instanceId).toString();
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 97e09b8..06eeb47c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -49,6 +49,7 @@
 class HostStubGen(val options: HostStubGenOptions) {
     fun run() {
         val errors = HostStubGenErrors()
+        val stats = HostStubGenStats()
 
         // Load all classes.
         val allClasses = loadClassStructures(options.inJar.get)
@@ -80,7 +81,14 @@
                 options.enableClassChecker.get,
                 allClasses,
                 errors,
+                stats,
         )
+
+        // Dump statistics, if specified.
+        options.statsFile.ifSet {
+            PrintWriter(it).use { pw -> stats.dump(pw) }
+            log.i("Dump file created at $it")
+        }
     }
 
     /**
@@ -237,6 +245,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar)
         log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled")
@@ -254,7 +263,8 @@
                         while (inEntries.hasMoreElements()) {
                             val entry = inEntries.nextElement()
                             convertSingleEntry(inZip, entry, stubOutStream, implOutStream,
-                                    filter, packageRedirector, enableChecker, classes, errors)
+                                    filter, packageRedirector, enableChecker, classes, errors,
+                                    stats)
                         }
                         log.i("Converted all entries.")
                     }
@@ -287,6 +297,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         log.d("Entry: %s", entry.name)
         log.withIndent {
@@ -300,7 +311,7 @@
             // If it's a class, convert it.
             if (name.endsWith(".class")) {
                 processSingleClass(inZip, entry, stubOutStream, implOutStream, filter,
-                        packageRedirector, enableChecker, classes, errors)
+                        packageRedirector, enableChecker, classes, errors, stats)
                 return
             }
 
@@ -354,6 +365,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         val classInternalName = entry.name.replaceFirst("\\.class$".toRegex(), "")
         val classPolicy = filter.getPolicyForClass(classInternalName)
@@ -370,7 +382,7 @@
                     stubOutStream.putNextEntry(newEntry)
                     convertClass(classInternalName, /*forImpl=*/false, bis,
                             stubOutStream, filter, packageRedirector, enableChecker, classes,
-                            errors)
+                            errors, stats)
                     stubOutStream.closeEntry()
                 }
             }
@@ -383,7 +395,7 @@
                     implOutStream.putNextEntry(newEntry)
                     convertClass(classInternalName, /*forImpl=*/true, bis,
                             implOutStream, filter, packageRedirector, enableChecker, classes,
-                            errors)
+                            errors, stats)
                     implOutStream.closeEntry()
                 }
             }
@@ -403,6 +415,7 @@
             enableChecker: Boolean,
             classes: ClassNodes,
             errors: HostStubGenErrors,
+            stats: HostStubGenStats,
             ) {
         val cr = ClassReader(input)
 
@@ -420,6 +433,7 @@
                 enablePostTrace = options.enablePostTrace.get,
                 enableNonStubMethodCallDetection = options.enableNonStubMethodCallDetection.get,
                 errors = errors,
+                stats = stats,
         )
         outVisitor = BaseAdapter.getVisitor(classInternalName, classes, outVisitor, filter,
                 packageRedirector, forImpl, visitorOptions)
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index d2ead18..9f5d524 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -108,6 +108,8 @@
         var enablePostTrace: SetOnce<Boolean> = SetOnce(false),
 
         var enableNonStubMethodCallDetection: SetOnce<Boolean> = SetOnce(false),
+
+        var statsFile: SetOnce<String?> = SetOnce(null),
 ) {
     companion object {
 
@@ -252,6 +254,8 @@
                         "--verbose-log" -> setLogFile(LogLevel.Verbose, nextArg())
                         "--debug-log" -> setLogFile(LogLevel.Debug, nextArg())
 
+                        "--stats-file" -> ret.statsFile.setNextStringArg()
+
                         else -> throw ArgumentsException("Unknown option: $arg")
                     }
                 } catch (e: SetOnce.SetMoreThanOnceException) {
@@ -387,6 +391,7 @@
               enablePreTrace=$enablePreTrace,
               enablePostTrace=$enablePostTrace,
               enableNonStubMethodCallDetection=$enableNonStubMethodCallDetection,
+              statsFile=$statsFile,
             }
             """.trimIndent()
     }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
new file mode 100644
index 0000000..fe4072f
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
@@ -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.hoststubgen
+
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.filters.FilterPolicyWithReason
+import java.io.PrintWriter
+
+open class HostStubGenStats {
+    data class Stats(
+            var supported: Int = 0,
+            var total: Int = 0,
+            val children: MutableMap<String, Stats> = mutableMapOf<String, Stats>(),
+    )
+
+    private val stats = mutableMapOf<String, Stats>()
+
+    fun onVisitPolicyForMethod(fullClassName: String, policy: FilterPolicyWithReason) {
+        if (policy.isIgnoredForStats) return
+
+        val packageName = resolvePackageName(fullClassName)
+        val className = resolveClassName(fullClassName)
+
+        val packageStats = stats.getOrPut(packageName) { Stats() }
+        val classStats = packageStats.children.getOrPut(className) { Stats() }
+
+        if (policy.policy.isSupported) {
+            packageStats.supported += 1
+            classStats.supported += 1
+        }
+        packageStats.total += 1
+        classStats.total += 1
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.printf("PackageName,ClassName,SupportedMethods,TotalMethods\n")
+        stats.forEach { (packageName, packageStats) ->
+            if (packageStats.supported > 0) {
+                packageStats.children.forEach { (className, classStats) ->
+                    pw.printf("%s,%s,%d,%d\n", packageName, className,
+                            classStats.supported, classStats.total)
+                }
+            }
+        }
+    }
+
+    private fun resolvePackageName(fullClassName: String): String {
+        val start = fullClassName.lastIndexOf('/')
+        return fullClassName.substring(0, start).toHumanReadableClassName()
+    }
+
+    private fun resolveClassName(fullClassName: String): String {
+        val start = fullClassName.lastIndexOf('/')
+        val end = fullClassName.indexOf('$')
+        if (end == -1) {
+            return fullClassName.substring(start + 1)
+        } else {
+            return fullClassName.substring(start + 1, end)
+        }
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
index 9317996..4d21106 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
@@ -111,6 +111,16 @@
             }
         }
 
+    /** Returns whether a policy is considered supported. */
+    val isSupported: Boolean
+        get() {
+            return when (this) {
+                // TODO: handle native method with no substitution as being unsupported
+                Stub, StubClass, Keep, KeepClass, SubstituteAndStub, SubstituteAndKeep -> true
+                else -> false
+            }
+        }
+
     fun getSubstitutionBasePolicy(): FilterPolicy {
         return when (this) {
             SubstituteAndKeep -> Keep
@@ -136,4 +146,4 @@
     fun withReason(reason: String): FilterPolicyWithReason {
         return FilterPolicyWithReason(this, reason)
     }
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
index b64a2f5..53eb5a8 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
@@ -63,4 +63,15 @@
     override fun toString(): String {
         return "[$policy - reason: $reason]"
     }
-}
\ No newline at end of file
+
+    /** Returns whether this policy should be ignored for stats. */
+    val isIgnoredForStats: Boolean
+        get() {
+            return reason.contains("anonymous-inner-class")
+                    || reason.contains("is-annotation")
+                    || reason.contains("is-enum")
+                    || reason.contains("is-synthetic-method")
+                    || reason.contains("special-class")
+                    || reason.contains("substitute-from")
+        }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index ea7d1d0..78b13fd 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -119,14 +119,14 @@
             if (cn.isEnum()) {
                 mn?.let { mn ->
                     if (isAutoGeneratedEnumMember(mn)) {
-                        return memberPolicy.withReason(classPolicy.reason).wrapReason("enum")
+                        return memberPolicy.withReason(classPolicy.reason).wrapReason("is-enum")
                     }
                 }
             }
 
             // Keep (or stub) all members of annotations.
             if (cn.isAnnotation()) {
-                return memberPolicy.withReason(classPolicy.reason).wrapReason("annotation")
+                return memberPolicy.withReason(classPolicy.reason).wrapReason("is-annotation")
             }
 
             mn?.let {
@@ -134,7 +134,7 @@
                     // For synthetic methods (such as lambdas), let's just inherit the class's
                     // policy.
                     return memberPolicy.withReason(classPolicy.reason).wrapReason(
-                            "synthetic method")
+                            "is-synthetic-method")
                 }
             }
         }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 7fdd944..6ad83fb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -132,23 +132,24 @@
                                             throw ParseException(
                                                     "Policy for AIDL classes already defined")
                                         }
-                                        aidlPolicy = policy.withReason("$FILTER_REASON (AIDL)")
+                                        aidlPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class AIDL)")
                                     }
                                     SpecialClass.FeatureFlags -> {
                                         if (featureFlagsPolicy != null) {
                                             throw ParseException(
                                                     "Policy for feature flags already defined")
                                         }
-                                        featureFlagsPolicy =
-                                                policy.withReason("$FILTER_REASON (feature flags)")
+                                        featureFlagsPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class feature flags)")
                                     }
                                     SpecialClass.Sysprops -> {
                                         if (syspropsPolicy != null) {
                                             throw ParseException(
                                                     "Policy for sysprops already defined")
                                         }
-                                        syspropsPolicy =
-                                                policy.withReason("$FILTER_REASON (sysprops)")
+                                        syspropsPolicy = policy.withReason(
+                                                "$FILTER_REASON (special-class sysprops)")
                                     }
                                 }
                             }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
index 21cfd4b..c20aa8b 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -16,6 +16,7 @@
 package com.android.hoststubgen.visitors
 
 import com.android.hoststubgen.HostStubGenErrors
+import com.android.hoststubgen.HostStubGenStats
 import com.android.hoststubgen.LogLevel
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.UnifiedVisitor
@@ -50,6 +51,7 @@
      */
     data class Options (
             val errors: HostStubGenErrors,
+            val stats: HostStubGenStats,
             val enablePreTrace: Boolean,
             val enablePostTrace: Boolean,
             val enableNonStubMethodCallDetection: Boolean,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index 416b782..beca945 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -141,6 +141,11 @@
             substituted: Boolean,
             superVisitor: MethodVisitor?,
     ): MethodVisitor? {
+        // Record statistics about visiting this method when visible.
+        if ((access and Opcodes.ACC_PRIVATE) == 0) {
+            options.stats.onVisitPolicyForMethod(currentClassName, policy)
+        }
+
         // Inject method log, if needed.
         var innerVisitor = superVisitor