Merge "Implement screen recording detection API internals" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index edd9d3a..2ae72ef 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -73,6 +73,8 @@
     ":android.provider.flags-aconfig-java{.generated_srcjars}",
     ":android.chre.flags-aconfig-java{.generated_srcjars}",
     ":android.speech.flags-aconfig-java{.generated_srcjars}",
+    ":power_flags_lib{.generated_srcjars}",
+    ":android.content.flags-aconfig-java{.generated_srcjars}",
 ]
 
 filegroup {
@@ -454,6 +456,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "com.android.media.flags.bettertogether-aconfig-java-host",
+    aconfig_declarations: "com.android.media.flags.bettertogether-aconfig",
+    host_supported: true,
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Media TV
 aconfig_declarations {
     name: "android.media.tv.flags-aconfig",
@@ -932,3 +941,23 @@
     aconfig_declarations: "android.speech.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Power
+java_aconfig_library {
+    name: "power_flags_lib",
+    aconfig_declarations: "power_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+// Content
+aconfig_declarations {
+    name: "android.content.flags-aconfig",
+    package: "android.content.flags",
+    srcs: ["core/java/android/content/flags/flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.content.flags-aconfig-java",
+    aconfig_declarations: "android.content.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/Android.bp b/Android.bp
index a3e39018..485f1be 100644
--- a/Android.bp
+++ b/Android.bp
@@ -149,6 +149,7 @@
         ":framework-javastream-protos",
         ":statslog-framework-java-gen", // FrameworkStatsLog.java
         ":audio_policy_configuration_V7_0",
+        ":perfetto_trace_javastream_protos",
     ],
 }
 
diff --git a/Ravenwood.bp b/Ravenwood.bp
index f330ad1..d13c4d7 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -75,16 +75,35 @@
     ],
 }
 
+java_library {
+    name: "mockito-ravenwood-prebuilt",
+    installable: false,
+    static_libs: [
+        "mockito-robolectric-prebuilt",
+    ],
+}
+
+java_library {
+    name: "inline-mockito-ravenwood-prebuilt",
+    installable: false,
+    static_libs: [
+        "inline-mockito-robolectric-prebuilt",
+    ],
+}
+
 android_ravenwood_libgroup {
     name: "ravenwood-runtime",
     libs: [
         "framework-minus-apex.ravenwood",
         "hoststubgen-helper-runtime.ravenwood",
         "hoststubgen-helper-framework-runtime.ravenwood",
+        "all-updatable-modules-system-stubs",
         "junit",
         "truth",
         "ravenwood-junit-impl",
         "android.test.mock.ravenwood",
+        "mockito-ravenwood-prebuilt",
+        "inline-mockito-ravenwood-prebuilt",
     ],
 }
 
@@ -94,5 +113,7 @@
         "junit",
         "truth",
         "ravenwood-junit",
+        "mockito-ravenwood-prebuilt",
+        "inline-mockito-ravenwood-prebuilt",
     ],
 }
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ecfd86c..d59775f 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -138,14 +138,15 @@
     }
   ],
   "postsubmit-ravenwood": [
-    {
-      "name": "CtsUtilTestCasesRavenwood",
-      "host": true,
-      "file_patterns": [
-        "*Ravenwood*",
-        "*ravenwood*"
-      ]
-    }
+    // TODO(ravenwood) promote it to presubmit
+    // TODO: Enable it once the infra knows how to run it.
+//    {
+//      "name": "CtsUtilTestCasesRavenwood",
+//      "file_patterns": [
+//        "*Ravenwood*",
+//        "*ravenwood*"
+//      ]
+//    }
   ],
   "postsubmit-managedprofile-stress": [
     {
diff --git a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java
index caf7e7f..1fc888b 100644
--- a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.PowerExemptionManager;
 import android.os.PowerExemptionManager.ReasonCode;
@@ -77,6 +78,9 @@
 
     int[] getPowerSaveTempWhitelistAppIds();
 
+    @NonNull
+    String[] getFullPowerWhitelistExceptIdle();
+
     /**
      * Listener to be notified when DeviceIdleController determines that the device has moved or is
      * stationary.
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index 6383ed8..31214cb 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -2374,6 +2374,11 @@
             return DeviceIdleController.this.isAppOnWhitelistInternal(appid);
         }
 
+        @Override
+        public String[] getFullPowerWhitelistExceptIdle() {
+            return DeviceIdleController.this.getFullPowerWhitelistInternalUnchecked();
+        }
+
         /**
          * Returns the array of app ids whitelisted by user. Take care not to
          * modify this, as it is a reference to the original copy. But the reference
@@ -3100,10 +3105,14 @@
     }
 
     private String[] getFullPowerWhitelistInternal(final int callingUid, final int callingUserId) {
-        final String[] apps;
+        return ArrayUtils.filter(getFullPowerWhitelistInternalUnchecked(), String[]::new,
+                (pkg) -> !mPackageManagerInternal.filterAppAccess(pkg, callingUid, callingUserId));
+    }
+
+    private String[] getFullPowerWhitelistInternalUnchecked() {
         synchronized (this) {
             int size = mPowerSaveWhitelistApps.size() + mPowerSaveWhitelistUserApps.size();
-            apps = new String[size];
+            final String[] apps = new String[size];
             int cur = 0;
             for (int i = 0; i < mPowerSaveWhitelistApps.size(); i++) {
                 apps[cur] = mPowerSaveWhitelistApps.keyAt(i);
@@ -3113,9 +3122,8 @@
                 apps[cur] = mPowerSaveWhitelistUserApps.keyAt(i);
                 cur++;
             }
+            return apps;
         }
-        return ArrayUtils.filter(apps, String[]::new,
-                (pkg) -> !mPackageManagerInternal.filterAppAccess(pkg, callingUid, callingUserId));
     }
 
     public boolean isPowerSaveWhitelistExceptIdleAppInternal(String packageName) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
index e3ba50d..e3ac780 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -318,6 +318,10 @@
 
         try {
             final boolean isStopped = mPackageManagerInternal.isPackageStopped(packageName, uid);
+            if (DEBUG) {
+                Slog.d(TAG,
+                        "Pulled stopped state of " + packageName + " (" + uid + "): " + isStopped);
+            }
             mPackageStoppedState.add(uid, packageName, isStopped);
             return isStopped;
         } catch (PackageManager.NameNotFoundException e) {
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 0e67b9a..2c9af67 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
@@ -30,11 +30,15 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PowerManager;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.util.ArraySet;
@@ -48,6 +52,8 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.AppSchedulingModuleThread;
+import com.android.server.DeviceIdleInternal;
+import com.android.server.LocalServices;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.utils.AlarmQueue;
 
@@ -127,6 +133,19 @@
     @GuardedBy("mLock")
     private final SparseLongArray mLastSeenConstraintTimesElapsed = new SparseLongArray();
 
+    private DeviceIdleInternal mDeviceIdleInternal;
+    private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>();
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
+                    mHandler.post(FlexibilityController.this::updatePowerAllowlistCache);
+                    break;
+            }
+        }
+    };
     @VisibleForTesting
     @GuardedBy("mLock")
     final FlexibilityTracker mFlexibilityTracker;
@@ -180,8 +199,16 @@
                 }
             };
 
-    private static final int MSG_UPDATE_JOBS = 0;
-    private static final int MSG_UPDATE_JOB = 1;
+    private static final int MSG_CHECK_ALL_JOBS = 0;
+    /** Check the jobs in {@link #mJobsToCheck} */
+    private static final int MSG_CHECK_JOBS = 1;
+    /** Check the jobs of packages in {@link #mPackagesToCheck} */
+    private static final int MSG_CHECK_PACKAGES = 2;
+
+    @GuardedBy("mLock")
+    private final ArraySet<JobStatus> mJobsToCheck = new ArraySet<>();
+    @GuardedBy("mLock")
+    private final ArraySet<String> mPackagesToCheck = new ArraySet<>();
 
     public FlexibilityController(
             JobSchedulerService service, PrefetchController prefetchController) {
@@ -204,6 +231,16 @@
         mPercentToDropConstraints =
                 mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
         mPrefetchController = prefetchController;
+
+        if (mFlexibilityEnabled) {
+            registerBroadcastReceiver();
+        }
+    }
+
+    @Override
+    public void onSystemServicesReady() {
+        mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class);
+        mHandler.post(FlexibilityController.this::updatePowerAllowlistCache);
     }
 
     @Override
@@ -241,6 +278,7 @@
             mFlexibilityAlarmQueue.removeAlarmForKey(js);
             mFlexibilityTracker.remove(js);
         }
+        mJobsToCheck.remove(js);
     }
 
     @Override
@@ -266,7 +304,14 @@
     @GuardedBy("mLock")
     boolean isFlexibilitySatisfiedLocked(JobStatus js) {
         return !mFlexibilityEnabled
+                // Exclude all jobs of the TOP app
                 || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP
+                // Only exclude DEFAULT+ priority jobs for BFGS+ apps
+                || (mService.getUidBias(js.getSourceUid()) >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+                        && js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT)
+                // For apps in the power allowlist, automatically exclude DEFAULT+ priority jobs.
+                || (js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT
+                        && mPowerAllowlistedApps.contains(js.getSourcePackageName()))
                 || hasEnoughSatisfiedConstraintsLocked(js)
                 || mService.isCurrentlyRunningLocked(js);
     }
@@ -371,7 +416,7 @@
 
                 // Push the job update to the handler to avoid blocking other controllers and
                 // potentially batch back-to-back controller state updates together.
-                mHandler.obtainMessage(MSG_UPDATE_JOBS).sendToTarget();
+                mHandler.obtainMessage(MSG_CHECK_ALL_JOBS).sendToTarget();
             }
         }
     }
@@ -485,7 +530,9 @@
     @Override
     @GuardedBy("mLock")
     public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
-        if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) {
+        if (prevBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+                && newBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) {
+            // All changes are below BFGS. There's no significant change to care about.
             return;
         }
         final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -557,6 +604,39 @@
         mFcConfig.processConstantLocked(properties, key);
     }
 
+    private void registerBroadcastReceiver() {
+        IntentFilter filter = new IntentFilter(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+        mContext.registerReceiver(mBroadcastReceiver, filter);
+    }
+
+    private void unregisterBroadcastReceiver() {
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
+    private void updatePowerAllowlistCache() {
+        if (mDeviceIdleInternal == null) {
+            return;
+        }
+
+        // Don't call out to DeviceIdleController with the lock held.
+        final String[] allowlistedPkgs = mDeviceIdleInternal.getFullPowerWhitelistExceptIdle();
+        final ArraySet<String> changedPkgs = new ArraySet<>();
+        synchronized (mLock) {
+            changedPkgs.addAll(mPowerAllowlistedApps);
+            mPowerAllowlistedApps.clear();
+            for (final String pkgName : allowlistedPkgs) {
+                mPowerAllowlistedApps.add(pkgName);
+                if (changedPkgs.contains(pkgName)) {
+                    changedPkgs.remove(pkgName);
+                } else {
+                    changedPkgs.add(pkgName);
+                }
+            }
+            mPackagesToCheck.addAll(changedPkgs);
+            mHandler.sendEmptyMessage(MSG_CHECK_PACKAGES);
+        }
+    }
+
     @VisibleForTesting
     class FlexibilityTracker {
         final ArrayList<ArraySet<JobStatus>> mTrackedJobs;
@@ -710,7 +790,8 @@
                     }
                     mFlexibilityTracker.setNumDroppedFlexibleConstraints(js,
                             js.getNumAppliedFlexibleConstraints());
-                    mHandler.obtainMessage(MSG_UPDATE_JOB, js).sendToTarget();
+                    mJobsToCheck.add(js);
+                    mHandler.sendEmptyMessage(MSG_CHECK_JOBS);
                     return;
                 }
                 if (nextTimeElapsed == NO_LIFECYCLE_END) {
@@ -761,10 +842,12 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_UPDATE_JOBS:
-                    removeMessages(MSG_UPDATE_JOBS);
+                case MSG_CHECK_ALL_JOBS:
+                    removeMessages(MSG_CHECK_ALL_JOBS);
 
                     synchronized (mLock) {
+                        mJobsToCheck.clear();
+                        mPackagesToCheck.clear();
                         final long nowElapsed = sElapsedRealtimeClock.millis();
                         final ArraySet<JobStatus> changedJobs = new ArraySet<>();
 
@@ -790,19 +873,50 @@
                     }
                     break;
 
-                case MSG_UPDATE_JOB:
+                case MSG_CHECK_JOBS:
                     synchronized (mLock) {
-                        final JobStatus js = (JobStatus) msg.obj;
-                        if (DEBUG) {
-                            Slog.d("blah", "Checking on " + js.toShortString());
-                        }
                         final long nowElapsed = sElapsedRealtimeClock.millis();
-                        if (js.setFlexibilityConstraintSatisfied(
-                                nowElapsed, isFlexibilitySatisfiedLocked(js))) {
-                            // TODO(141645789): add method that will take a single job
-                            ArraySet<JobStatus> changedJob = new ArraySet<>();
-                            changedJob.add(js);
-                            mStateChangedListener.onControllerStateChanged(changedJob);
+                        ArraySet<JobStatus> changedJobs = new ArraySet<>();
+
+                        for (int i = mJobsToCheck.size() - 1; i >= 0; --i) {
+                            final JobStatus js = mJobsToCheck.valueAt(i);
+                            if (DEBUG) {
+                                Slog.d(TAG, "Checking on " + js.toShortString());
+                            }
+                            if (js.setFlexibilityConstraintSatisfied(
+                                    nowElapsed, isFlexibilitySatisfiedLocked(js))) {
+                                changedJobs.add(js);
+                            }
+                        }
+
+                        mJobsToCheck.clear();
+                        if (changedJobs.size() > 0) {
+                            mStateChangedListener.onControllerStateChanged(changedJobs);
+                        }
+                    }
+                    break;
+
+                case MSG_CHECK_PACKAGES:
+                    synchronized (mLock) {
+                        final long nowElapsed = sElapsedRealtimeClock.millis();
+                        final ArraySet<JobStatus> changedJobs = new ArraySet<>();
+
+                        mService.getJobStore().forEachJob(
+                                (js) -> mPackagesToCheck.contains(js.getSourcePackageName())
+                                        || mPackagesToCheck.contains(js.getCallingPackageName()),
+                                (js) -> {
+                                    if (DEBUG) {
+                                        Slog.d(TAG, "Checking on " + js.toShortString());
+                                    }
+                                    if (js.setFlexibilityConstraintSatisfied(
+                                            nowElapsed, isFlexibilitySatisfiedLocked(js))) {
+                                        changedJobs.add(js);
+                                    }
+                                });
+
+                        mPackagesToCheck.clear();
+                        if (changedJobs.size() > 0) {
+                            mStateChangedListener.onControllerStateChanged(changedJobs);
                         }
                     }
                     break;
@@ -882,10 +996,12 @@
                             mFlexibilityEnabled = true;
                             mPrefetchController
                                     .registerPrefetchChangedListener(mPrefetchChangedListener);
+                            registerBroadcastReceiver();
                         } else {
                             mFlexibilityEnabled = false;
                             mPrefetchController
                                     .unRegisterPrefetchChangedListener(mPrefetchChangedListener);
+                            unregisterBroadcastReceiver();
                         }
                     }
                     break;
@@ -985,7 +1101,14 @@
             pw.println(":");
             pw.increaseIndent();
 
-            pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS).println();
+            pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS);
+            pw.print("(");
+            if (APPLIED_CONSTRAINTS != 0) {
+                JobStatus.dumpConstraints(pw, APPLIED_CONSTRAINTS);
+            } else {
+                pw.print("nothing");
+            }
+            pw.println(")");
             pw.print(KEY_DEADLINE_PROXIMITY_LIMIT, DEADLINE_PROXIMITY_LIMIT_MS).println();
             pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE, FALLBACK_FLEXIBILITY_DEADLINE_MS).println();
             pw.print(KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS,
@@ -1044,6 +1167,10 @@
         pw.decreaseIndent();
 
         pw.println();
+        pw.print("Power allowlisted packages: ");
+        pw.println(mPowerAllowlistedApps);
+
+        pw.println();
         mFlexibilityTracker.dump(pw, predicate);
         pw.println();
         mFlexibilityAlarmQueue.dump(pw);
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 74344cd..59c0128 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -239,6 +239,10 @@
     name: "android-non-updatable_from_source_defaults",
     libs: ["stub-annotations"],
     static_libs: ["framework-res-package-jar"], // Export package of framework-res
+}
+
+java_defaults {
+    name: "android-non-updatable_exportable_from_source_defaults",
     dist: {
         targets: ["sdk"],
         tag: ".jar",
@@ -265,6 +269,14 @@
 }
 
 java_library {
+    name: "android-non-updatable.stubs.exportable",
+    defaults: ["android-non-updatable_defaults"],
+    static_libs: [
+        "android-non-updatable.stubs.exportable.from-source",
+    ],
+}
+
+java_library {
     name: "android-non-updatable.stubs.system",
     defaults: ["android-non-updatable_defaults"],
     static_libs: [
@@ -283,6 +295,14 @@
 }
 
 java_library {
+    name: "android-non-updatable.stubs.exportable.system",
+    defaults: ["android-non-updatable_defaults"],
+    static_libs: [
+        "android-non-updatable.stubs.exportable.system.from-source",
+    ],
+}
+
+java_library {
     name: "android-non-updatable.stubs.module_lib",
     defaults: ["android-non-updatable_defaults"],
     static_libs: [
@@ -301,6 +321,14 @@
 }
 
 java_library {
+    name: "android-non-updatable.stubs.exportable.module_lib",
+    defaults: ["android-non-updatable_defaults"],
+    static_libs: [
+        "android-non-updatable.stubs.exportable.module_lib.from-source",
+    ],
+}
+
+java_library {
     name: "android-non-updatable.stubs.test",
     defaults: ["android-non-updatable_defaults"],
     static_libs: [
@@ -319,6 +347,14 @@
 }
 
 java_library {
+    name: "android-non-updatable.stubs.exportable.test",
+    defaults: ["android-non-updatable_defaults"],
+    static_libs: [
+        "android-non-updatable.stubs.exportable.test.from-source",
+    ],
+}
+
+java_library {
     name: "android-non-updatable.stubs.from-source",
     defaults: [
         "android-non-updatable_defaults",
@@ -326,6 +362,17 @@
     ],
     srcs: [":api-stubs-docs-non-updatable"],
     libs: ["all-modules-public-stubs"],
+}
+
+java_library {
+    name: "android-non-updatable.stubs.exportable.from-source",
+    defaults: [
+        "android-non-updatable_defaults",
+        "android-non-updatable_from_source_defaults",
+        "android-non-updatable_exportable_from_source_defaults",
+    ],
+    srcs: [":api-stubs-docs-non-updatable{.exportable}"],
+    libs: ["all-modules-public-stubs"],
     dist: {
         dir: "apistubs/android/public",
     },
@@ -339,6 +386,17 @@
     ],
     srcs: [":system-api-stubs-docs-non-updatable"],
     libs: ["all-modules-system-stubs"],
+}
+
+java_library {
+    name: "android-non-updatable.stubs.exportable.system.from-source",
+    defaults: [
+        "android-non-updatable_defaults",
+        "android-non-updatable_from_source_defaults",
+        "android-non-updatable_exportable_from_source_defaults",
+    ],
+    srcs: [":system-api-stubs-docs-non-updatable{.exportable}"],
+    libs: ["all-modules-system-stubs"],
     dist: {
         dir: "apistubs/android/system",
     },
@@ -352,6 +410,17 @@
     ],
     srcs: [":module-lib-api-stubs-docs-non-updatable"],
     libs: non_updatable_api_deps_on_modules,
+}
+
+java_library {
+    name: "android-non-updatable.stubs.exportable.module_lib.from-source",
+    defaults: [
+        "android-non-updatable_defaults",
+        "android-non-updatable_from_source_defaults",
+        "android-non-updatable_exportable_from_source_defaults",
+    ],
+    srcs: [":module-lib-api-stubs-docs-non-updatable{.exportable}"],
+    libs: non_updatable_api_deps_on_modules,
     dist: {
         dir: "apistubs/android/module-lib",
     },
@@ -365,6 +434,17 @@
     ],
     srcs: [":test-api-stubs-docs-non-updatable"],
     libs: ["all-modules-system-stubs"],
+}
+
+java_library {
+    name: "android-non-updatable.stubs.exportable.test.from-source",
+    defaults: [
+        "android-non-updatable_defaults",
+        "android-non-updatable_from_source_defaults",
+        "android-non-updatable_exportable_from_source_defaults",
+    ],
+    srcs: [":test-api-stubs-docs-non-updatable{.exportable}"],
+    libs: ["all-modules-system-stubs"],
     dist: {
         dir: "apistubs/android/test",
     },
@@ -462,6 +542,16 @@
 }
 
 java_library {
+    name: "android_stubs_current_exportable.from-source",
+    static_libs: [
+        "all-modules-public-stubs-exportable",
+        "android-non-updatable.stubs.exportable",
+        "private-stub-annotations-jar",
+    ],
+    defaults: ["android.jar_defaults"],
+}
+
+java_library {
     name: "android_system_stubs_current.from-source",
     static_libs: [
         "all-modules-system-stubs",
@@ -470,6 +560,19 @@
     ],
     defaults: [
         "android.jar_defaults",
+    ],
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library {
+    name: "android_system_stubs_current_exportable.from-source",
+    static_libs: [
+        "all-modules-system-stubs-exportable",
+        "android-non-updatable.stubs.exportable.system",
+        "private-stub-annotations-jar",
+    ],
+    defaults: [
+        "android.jar_defaults",
         "android_stubs_dists_default",
     ],
     dist: {
@@ -498,6 +601,23 @@
     ],
     defaults: [
         "android.jar_defaults",
+    ],
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library {
+    name: "android_test_stubs_current_exportable.from-source",
+    static_libs: [
+        // Updatable modules do not have test APIs, but we want to include their SystemApis, like we
+        // include the SystemApi of framework-non-updatable-sources.
+        "all-updatable-modules-system-stubs-exportable",
+        // Non-updatable modules on the other hand can have test APIs, so include their test-stubs.
+        "all-non-updatable-modules-test-stubs-exportable",
+        "android-non-updatable.stubs.exportable.test",
+        "private-stub-annotations-jar",
+    ],
+    defaults: [
+        "android.jar_defaults",
         "android_stubs_dists_default",
     ],
     dist: {
@@ -505,6 +625,7 @@
     },
 }
 
+// This module does not need to be copied to dist
 java_library {
     name: "android_test_frameworks_core_stubs_current.from-source",
     static_libs: [
@@ -513,24 +634,34 @@
     ],
     defaults: [
         "android.jar_defaults",
-        "android_stubs_dists_default",
     ],
-    dist: {
-        dir: "apistubs/android/test-core",
-    },
+    visibility: ["//frameworks/base/services"],
 }
 
 java_library {
     name: "android_module_lib_stubs_current.from-source",
     defaults: [
         "android.jar_defaults",
-        "android_stubs_dists_default",
     ],
     static_libs: [
         "android-non-updatable.stubs.module_lib",
         "art.module.public.api.stubs.module_lib",
         "i18n.module.public.api.stubs",
     ],
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library {
+    name: "android_module_lib_stubs_current_exportable.from-source",
+    defaults: [
+        "android.jar_defaults",
+        "android_stubs_dists_default",
+    ],
+    static_libs: [
+        "android-non-updatable.stubs.exportable.module_lib",
+        "art.module.public.api.stubs.exportable.module_lib",
+        "i18n.module.public.api.stubs.exportable",
+    ],
     dist: {
         dir: "apistubs/android/module-lib",
     },
@@ -540,13 +671,26 @@
     name: "android_system_server_stubs_current.from-source",
     defaults: [
         "android.jar_defaults",
-        "android_stubs_dists_default",
     ],
     srcs: [":services-non-updatable-stubs"],
     installable: false,
     static_libs: [
         "android_module_lib_stubs_current.from-source",
     ],
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library {
+    name: "android_system_server_stubs_current_exportable.from-source",
+    defaults: [
+        "android.jar_defaults",
+        "android_stubs_dists_default",
+    ],
+    srcs: [":services-non-updatable-stubs{.exportable}"],
+    installable: false,
+    static_libs: [
+        "android_module_lib_stubs_current_exportable.from-source",
+    ],
     dist: {
         dir: "apistubs/android/system-server",
     },
diff --git a/api/api.go b/api/api.go
index b975c55..fa2be21 100644
--- a/api/api.go
+++ b/api/api.go
@@ -204,6 +204,15 @@
 	ctx.CreateModule(java.LibraryFactory, &props)
 }
 
+func createMergedPublicExportableStubs(ctx android.LoadHookContext, modules []string) {
+	props := libraryProps{}
+	props.Name = proptools.StringPtr("all-modules-public-stubs-exportable")
+	props.Static_libs = transformArray(modules, "", ".stubs.exportable")
+	props.Sdk_version = proptools.StringPtr("module_current")
+	props.Visibility = []string{"//frameworks/base"}
+	ctx.CreateModule(java.LibraryFactory, &props)
+}
+
 func createMergedSystemStubs(ctx android.LoadHookContext, modules []string) {
 	// First create the all-updatable-modules-system-stubs
 	{
@@ -228,6 +237,30 @@
 	}
 }
 
+func createMergedSystemExportableStubs(ctx android.LoadHookContext, modules []string) {
+	// First create the all-updatable-modules-system-stubs
+	{
+		updatable_modules := removeAll(modules, non_updatable_modules)
+		props := libraryProps{}
+		props.Name = proptools.StringPtr("all-updatable-modules-system-stubs-exportable")
+		props.Static_libs = transformArray(updatable_modules, "", ".stubs.exportable.system")
+		props.Sdk_version = proptools.StringPtr("module_current")
+		props.Visibility = []string{"//frameworks/base"}
+		ctx.CreateModule(java.LibraryFactory, &props)
+	}
+	// Now merge all-updatable-modules-system-stubs and stubs from non-updatable modules
+	// into all-modules-system-stubs.
+	{
+		props := libraryProps{}
+		props.Name = proptools.StringPtr("all-modules-system-stubs-exportable")
+		props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.exportable.system")
+		props.Static_libs = append(props.Static_libs, "all-updatable-modules-system-stubs-exportable")
+		props.Sdk_version = proptools.StringPtr("module_current")
+		props.Visibility = []string{"//frameworks/base"}
+		ctx.CreateModule(java.LibraryFactory, &props)
+	}
+}
+
 func createMergedTestStubsForNonUpdatableModules(ctx android.LoadHookContext) {
 	props := libraryProps{}
 	props.Name = proptools.StringPtr("all-non-updatable-modules-test-stubs")
@@ -237,6 +270,15 @@
 	ctx.CreateModule(java.LibraryFactory, &props)
 }
 
+func createMergedTestExportableStubsForNonUpdatableModules(ctx android.LoadHookContext) {
+	props := libraryProps{}
+	props.Name = proptools.StringPtr("all-non-updatable-modules-test-stubs-exportable")
+	props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.exportable.test")
+	props.Sdk_version = proptools.StringPtr("module_current")
+	props.Visibility = []string{"//frameworks/base"}
+	ctx.CreateModule(java.LibraryFactory, &props)
+}
+
 func createMergedFrameworkImpl(ctx android.LoadHookContext, modules []string) {
 	// This module is for the "framework-all" module, which should not include the core libraries.
 	modules = removeAll(modules, core_libraries_modules)
@@ -267,6 +309,19 @@
 	}
 }
 
+func createMergedFrameworkModuleLibExportableStubs(ctx android.LoadHookContext, modules []string) {
+	// The user of this module compiles against the "core" SDK and against non-updatable modules,
+	// so remove to avoid dupes.
+	modules = removeAll(modules, core_libraries_modules)
+	modules = removeAll(modules, non_updatable_modules)
+	props := libraryProps{}
+	props.Name = proptools.StringPtr("framework-updatable-stubs-module_libs_api-exportable")
+	props.Static_libs = transformArray(modules, "", ".stubs.exportable.module_lib")
+	props.Sdk_version = proptools.StringPtr("module_current")
+	props.Visibility = []string{"//frameworks/base"}
+	ctx.CreateModule(java.LibraryFactory, &props)
+}
+
 func createMergedFrameworkModuleLibStubs(ctx android.LoadHookContext, modules []string) {
 	// The user of this module compiles against the "core" SDK and against non-updatable modules,
 	// so remove to avoid dupes.
@@ -382,6 +437,27 @@
 	}
 }
 
+func createFullExportableApiLibraries(ctx android.LoadHookContext) {
+	javaLibraryNames := []string{
+		"android_stubs_current_exportable",
+		"android_system_stubs_current_exportable",
+		"android_test_stubs_current_exportable",
+		"android_module_lib_stubs_current_exportable",
+		"android_system_server_stubs_current_exportable",
+	}
+
+	for _, libraryName := range javaLibraryNames {
+		props := libraryProps{}
+		props.Name = proptools.StringPtr(libraryName)
+		staticLib := libraryName + ".from-source"
+		props.Static_libs = []string{staticLib}
+		props.Defaults = []string{"android.jar_defaults"}
+		props.Visibility = []string{"//visibility:public"}
+
+		ctx.CreateModule(java.LibraryFactory, &props)
+	}
+}
+
 func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) {
 	bootclasspath := a.properties.Bootclasspath
 	system_server_classpath := a.properties.System_server_classpath
@@ -397,6 +473,11 @@
 	createMergedFrameworkModuleLibStubs(ctx, bootclasspath)
 	createMergedFrameworkImpl(ctx, bootclasspath)
 
+	createMergedPublicExportableStubs(ctx, bootclasspath)
+	createMergedSystemExportableStubs(ctx, bootclasspath)
+	createMergedTestExportableStubsForNonUpdatableModules(ctx)
+	createMergedFrameworkModuleLibExportableStubs(ctx, bootclasspath)
+
 	createMergedAnnotationsFilegroups(ctx, bootclasspath, system_server_classpath)
 
 	createPublicStubsSourceFilegroup(ctx, bootclasspath)
@@ -404,6 +485,8 @@
 	createApiContributionDefaults(ctx, bootclasspath)
 
 	createFullApiLibraries(ctx)
+
+	createFullExportableApiLibraries(ctx)
 }
 
 func combinedApisModuleFactory() android.Module {
diff --git a/boot/Android.bp b/boot/Android.bp
index 8a3d35e..c4a1139b7 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -26,9 +26,10 @@
 soong_config_module_type {
     name: "custom_platform_bootclasspath",
     module_type: "platform_bootclasspath",
-    config_namespace: "AUTO",
+    config_namespace: "bootclasspath",
     bool_variables: [
         "car_bootclasspath_fragment",
+        "nfc_apex_bootclasspath_fragment",
     ],
     properties: [
         "fragments",
@@ -155,6 +156,15 @@
                 },
             ],
         },
+        nfc_apex_bootclasspath_fragment: {
+            fragments: [
+                // only used if NFC mainline is enabled.
+                {
+                    apex: "com.android.nfcservices",
+                    module: "com.android.nfcservices-bootclasspath-fragment",
+                },
+            ],
+        },
     },
 
     // Additional information needed by hidden api processing.
diff --git a/boot/hiddenapi/hiddenapi-max-target-o.txt b/boot/hiddenapi/hiddenapi-max-target-o.txt
index 3c16915..2d417ce 100644
--- a/boot/hiddenapi/hiddenapi-max-target-o.txt
+++ b/boot/hiddenapi/hiddenapi-max-target-o.txt
@@ -33834,649 +33834,6 @@
 Landroid/net/WifiLinkQualityInfo;->setTxBad(J)V
 Landroid/net/WifiLinkQualityInfo;->setTxGood(J)V
 Landroid/net/WifiLinkQualityInfo;->setType(I)V
-Landroid/nfc/ApduList;-><init>()V
-Landroid/nfc/ApduList;-><init>(Landroid/os/Parcel;)V
-Landroid/nfc/ApduList;->add([B)V
-Landroid/nfc/ApduList;->commands:Ljava/util/ArrayList;
-Landroid/nfc/ApduList;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/ApduList;->get()Ljava/util/List;
-Landroid/nfc/BeamShareData;-><init>(Landroid/nfc/NdefMessage;[Landroid/net/Uri;Landroid/os/UserHandle;I)V
-Landroid/nfc/BeamShareData;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/BeamShareData;->flags:I
-Landroid/nfc/BeamShareData;->ndefMessage:Landroid/nfc/NdefMessage;
-Landroid/nfc/BeamShareData;->uris:[Landroid/net/Uri;
-Landroid/nfc/BeamShareData;->userHandle:Landroid/os/UserHandle;
-Landroid/nfc/cardemulation/AidGroup;-><init>(Ljava/util/List;Ljava/lang/String;)V
-Landroid/nfc/cardemulation/AidGroup;->isValidCategory(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/AidGroup;->MAX_NUM_AIDS:I
-Landroid/nfc/cardemulation/AidGroup;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/ApduServiceInfo;->dump(Ljava/io/FileDescriptor;Ljava/io/PrintWriter;[Ljava/lang/String;)V
-Landroid/nfc/cardemulation/ApduServiceInfo;->getAidGroups()Ljava/util/ArrayList;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getAids()Ljava/util/List;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getCategoryForAid(Ljava/lang/String;)Ljava/lang/String;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getComponent()Landroid/content/ComponentName;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getDynamicAidGroupForCategory(Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getPrefixAids()Ljava/util/List;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getSubsetAids()Ljava/util/List;
-Landroid/nfc/cardemulation/ApduServiceInfo;->hasCategory(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/ApduServiceInfo;->loadAppLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence;
-Landroid/nfc/cardemulation/ApduServiceInfo;->loadIcon(Landroid/content/pm/PackageManager;)Landroid/graphics/drawable/Drawable;
-Landroid/nfc/cardemulation/ApduServiceInfo;->loadLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence;
-Landroid/nfc/cardemulation/ApduServiceInfo;->mBannerResourceId:I
-Landroid/nfc/cardemulation/ApduServiceInfo;->mDescription:Ljava/lang/String;
-Landroid/nfc/cardemulation/ApduServiceInfo;->mOnHost:Z
-Landroid/nfc/cardemulation/ApduServiceInfo;->mRequiresDeviceUnlock:Z
-Landroid/nfc/cardemulation/ApduServiceInfo;->mSettingsActivityName:Ljava/lang/String;
-Landroid/nfc/cardemulation/ApduServiceInfo;->mUid:I
-Landroid/nfc/cardemulation/ApduServiceInfo;->removeDynamicAidGroupForCategory(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/ApduServiceInfo;->setOrReplaceDynamicAidGroup(Landroid/nfc/cardemulation/AidGroup;)V
-Landroid/nfc/cardemulation/ApduServiceInfo;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/CardEmulation;-><init>(Landroid/content/Context;Landroid/nfc/INfcCardEmulation;)V
-Landroid/nfc/cardemulation/CardEmulation;->getServices(Ljava/lang/String;)Ljava/util/List;
-Landroid/nfc/cardemulation/CardEmulation;->isValidAid(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/CardEmulation;->mContext:Landroid/content/Context;
-Landroid/nfc/cardemulation/CardEmulation;->recoverService()V
-Landroid/nfc/cardemulation/CardEmulation;->sCardEmus:Ljava/util/HashMap;
-Landroid/nfc/cardemulation/CardEmulation;->setDefaultForNextTap(Landroid/content/ComponentName;)Z
-Landroid/nfc/cardemulation/CardEmulation;->setDefaultServiceForCategory(Landroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/CardEmulation;->sIsInitialized:Z
-Landroid/nfc/cardemulation/CardEmulation;->sService:Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/cardemulation/CardEmulation;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostApduService;->KEY_DATA:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostApduService;->mMessenger:Landroid/os/Messenger;
-Landroid/nfc/cardemulation/HostApduService;->mNfcService:Landroid/os/Messenger;
-Landroid/nfc/cardemulation/HostApduService;->MSG_COMMAND_APDU:I
-Landroid/nfc/cardemulation/HostApduService;->MSG_DEACTIVATED:I
-Landroid/nfc/cardemulation/HostApduService;->MSG_RESPONSE_APDU:I
-Landroid/nfc/cardemulation/HostApduService;->MSG_UNHANDLED:I
-Landroid/nfc/cardemulation/HostApduService;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostNfcFService;->KEY_DATA:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostNfcFService;->KEY_MESSENGER:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostNfcFService;->mMessenger:Landroid/os/Messenger;
-Landroid/nfc/cardemulation/HostNfcFService;->mNfcService:Landroid/os/Messenger;
-Landroid/nfc/cardemulation/HostNfcFService;->MSG_COMMAND_PACKET:I
-Landroid/nfc/cardemulation/HostNfcFService;->MSG_DEACTIVATED:I
-Landroid/nfc/cardemulation/HostNfcFService;->MSG_RESPONSE_PACKET:I
-Landroid/nfc/cardemulation/HostNfcFService;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFCardEmulation;-><init>(Landroid/content/Context;Landroid/nfc/INfcFCardEmulation;)V
-Landroid/nfc/cardemulation/NfcFCardEmulation;->getMaxNumOfRegisterableSystemCodes()I
-Landroid/nfc/cardemulation/NfcFCardEmulation;->getNfcFServices()Ljava/util/List;
-Landroid/nfc/cardemulation/NfcFCardEmulation;->isValidNfcid2(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/NfcFCardEmulation;->isValidSystemCode(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/NfcFCardEmulation;->mContext:Landroid/content/Context;
-Landroid/nfc/cardemulation/NfcFCardEmulation;->recoverService()V
-Landroid/nfc/cardemulation/NfcFCardEmulation;->sCardEmus:Ljava/util/HashMap;
-Landroid/nfc/cardemulation/NfcFCardEmulation;->sIsInitialized:Z
-Landroid/nfc/cardemulation/NfcFCardEmulation;->sService:Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/cardemulation/NfcFCardEmulation;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;-><init>(Landroid/content/pm/PackageManager;Landroid/content/pm/ResolveInfo;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;-><init>(Landroid/content/pm/ResolveInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->DEFAULT_T3T_PMM:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->dump(Ljava/io/FileDescriptor;Ljava/io/PrintWriter;[Ljava/lang/String;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getComponent()Landroid/content/ComponentName;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getDescription()Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getNfcid2()Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getSystemCode()Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getT3tPmm()Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getUid()I
-Landroid/nfc/cardemulation/NfcFServiceInfo;->loadIcon(Landroid/content/pm/PackageManager;)Landroid/graphics/drawable/Drawable;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->loadLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mDescription:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mDynamicNfcid2:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mDynamicSystemCode:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mNfcid2:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mService:Landroid/content/pm/ResolveInfo;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mSystemCode:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mT3tPmm:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mUid:I
-Landroid/nfc/cardemulation/NfcFServiceInfo;->setOrReplaceDynamicNfcid2(Ljava/lang/String;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;->setOrReplaceDynamicSystemCode(Ljava/lang/String;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;->TAG:Ljava/lang/String;
-Landroid/nfc/ErrorCodes;-><init>()V
-Landroid/nfc/ErrorCodes;->asString(I)Ljava/lang/String;
-Landroid/nfc/ErrorCodes;->ERROR_BUFFER_TO_SMALL:I
-Landroid/nfc/ErrorCodes;->ERROR_BUSY:I
-Landroid/nfc/ErrorCodes;->ERROR_CANCELLED:I
-Landroid/nfc/ErrorCodes;->ERROR_CONNECT:I
-Landroid/nfc/ErrorCodes;->ERROR_DISCONNECT:I
-Landroid/nfc/ErrorCodes;->ERROR_INSUFFICIENT_RESOURCES:I
-Landroid/nfc/ErrorCodes;->ERROR_INVALID_PARAM:I
-Landroid/nfc/ErrorCodes;->ERROR_IO:I
-Landroid/nfc/ErrorCodes;->ERROR_NFC_ON:I
-Landroid/nfc/ErrorCodes;->ERROR_NOT_INITIALIZED:I
-Landroid/nfc/ErrorCodes;->ERROR_NOT_SUPPORTED:I
-Landroid/nfc/ErrorCodes;->ERROR_NO_SE_CONNECTED:I
-Landroid/nfc/ErrorCodes;->ERROR_READ:I
-Landroid/nfc/ErrorCodes;->ERROR_SAP_USED:I
-Landroid/nfc/ErrorCodes;->ERROR_SERVICE_NAME_USED:I
-Landroid/nfc/ErrorCodes;->ERROR_SE_ALREADY_SELECTED:I
-Landroid/nfc/ErrorCodes;->ERROR_SE_CONNECTED:I
-Landroid/nfc/ErrorCodes;->ERROR_SOCKET_CREATION:I
-Landroid/nfc/ErrorCodes;->ERROR_SOCKET_NOT_CONNECTED:I
-Landroid/nfc/ErrorCodes;->ERROR_SOCKET_OPTIONS:I
-Landroid/nfc/ErrorCodes;->ERROR_TIMEOUT:I
-Landroid/nfc/ErrorCodes;->ERROR_WRITE:I
-Landroid/nfc/ErrorCodes;->SUCCESS:I
-Landroid/nfc/IAppCallback$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/IAppCallback$Stub$Proxy;->createBeamShareData(B)Landroid/nfc/BeamShareData;
-Landroid/nfc/IAppCallback$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/IAppCallback$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/IAppCallback$Stub$Proxy;->onNdefPushComplete(B)V
-Landroid/nfc/IAppCallback$Stub$Proxy;->onTagDiscovered(Landroid/nfc/Tag;)V
-Landroid/nfc/IAppCallback$Stub;-><init>()V
-Landroid/nfc/IAppCallback$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/IAppCallback;
-Landroid/nfc/IAppCallback$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/IAppCallback$Stub;->TRANSACTION_createBeamShareData:I
-Landroid/nfc/IAppCallback$Stub;->TRANSACTION_onNdefPushComplete:I
-Landroid/nfc/IAppCallback$Stub;->TRANSACTION_onTagDiscovered:I
-Landroid/nfc/IAppCallback;->createBeamShareData(B)Landroid/nfc/BeamShareData;
-Landroid/nfc/IAppCallback;->onNdefPushComplete(B)V
-Landroid/nfc/IAppCallback;->onTagDiscovered(Landroid/nfc/Tag;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->addNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;[I)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->disable(Z)Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->disableNdefPush()Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->dispatch(Landroid/nfc/Tag;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->enable()Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->enableNdefPush()Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcAdapterExtrasInterface(Ljava/lang/String;)Landroid/nfc/INfcAdapterExtras;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcCardEmulationInterface()Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcDtaInterface(Ljava/lang/String;)Landroid/nfc/INfcDta;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcFCardEmulationInterface()Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcTagInterface()Landroid/nfc/INfcTag;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getState()I
-Landroid/nfc/INfcAdapter$Stub$Proxy;->ignore(IILandroid/nfc/ITagRemovedCallback;)Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->invokeBeam()V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->invokeBeamInternal(Landroid/nfc/BeamShareData;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->isNdefPushEnabled()Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->pausePolling(I)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->removeNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->resumePolling()V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->setAppCallback(Landroid/nfc/IAppCallback;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->setForegroundDispatch(Landroid/app/PendingIntent;[Landroid/content/IntentFilter;Landroid/nfc/TechListParcel;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->setP2pModes(II)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->setReaderMode(Landroid/os/IBinder;Landroid/nfc/IAppCallback;ILandroid/os/Bundle;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->verifyNfcPermission()V
-Landroid/nfc/INfcAdapter$Stub;-><init>()V
-Landroid/nfc/INfcAdapter$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcAdapter;
-Landroid/nfc/INfcAdapter$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_addNfcUnlockHandler:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_disable:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_disableNdefPush:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_dispatch:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_enableNdefPush:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcAdapterExtrasInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcCardEmulationInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcDtaInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcFCardEmulationInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcTagInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getState:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_ignore:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_invokeBeam:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_invokeBeamInternal:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_isNdefPushEnabled:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_pausePolling:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_removeNfcUnlockHandler:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_resumePolling:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setAppCallback:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setForegroundDispatch:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setP2pModes:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setReaderMode:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_verifyNfcPermission:I
-Landroid/nfc/INfcAdapter;->addNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;[I)V
-Landroid/nfc/INfcAdapter;->disable(Z)Z
-Landroid/nfc/INfcAdapter;->disableNdefPush()Z
-Landroid/nfc/INfcAdapter;->dispatch(Landroid/nfc/Tag;)V
-Landroid/nfc/INfcAdapter;->enable()Z
-Landroid/nfc/INfcAdapter;->enableNdefPush()Z
-Landroid/nfc/INfcAdapter;->getNfcAdapterExtrasInterface(Ljava/lang/String;)Landroid/nfc/INfcAdapterExtras;
-Landroid/nfc/INfcAdapter;->getNfcCardEmulationInterface()Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/INfcAdapter;->getNfcDtaInterface(Ljava/lang/String;)Landroid/nfc/INfcDta;
-Landroid/nfc/INfcAdapter;->getNfcFCardEmulationInterface()Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/INfcAdapter;->getNfcTagInterface()Landroid/nfc/INfcTag;
-Landroid/nfc/INfcAdapter;->getState()I
-Landroid/nfc/INfcAdapter;->ignore(IILandroid/nfc/ITagRemovedCallback;)Z
-Landroid/nfc/INfcAdapter;->invokeBeam()V
-Landroid/nfc/INfcAdapter;->invokeBeamInternal(Landroid/nfc/BeamShareData;)V
-Landroid/nfc/INfcAdapter;->isNdefPushEnabled()Z
-Landroid/nfc/INfcAdapter;->pausePolling(I)V
-Landroid/nfc/INfcAdapter;->removeNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;)V
-Landroid/nfc/INfcAdapter;->resumePolling()V
-Landroid/nfc/INfcAdapter;->setAppCallback(Landroid/nfc/IAppCallback;)V
-Landroid/nfc/INfcAdapter;->setForegroundDispatch(Landroid/app/PendingIntent;[Landroid/content/IntentFilter;Landroid/nfc/TechListParcel;)V
-Landroid/nfc/INfcAdapter;->setP2pModes(II)V
-Landroid/nfc/INfcAdapter;->setReaderMode(Landroid/os/IBinder;Landroid/nfc/IAppCallback;ILandroid/os/Bundle;)V
-Landroid/nfc/INfcAdapter;->verifyNfcPermission()V
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->authenticate(Ljava/lang/String;[B)V
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->close(Ljava/lang/String;Landroid/os/IBinder;)Landroid/os/Bundle;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getCardEmulationRoute(Ljava/lang/String;)I
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getDriverName(Ljava/lang/String;)Ljava/lang/String;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->open(Ljava/lang/String;Landroid/os/IBinder;)Landroid/os/Bundle;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->setCardEmulationRoute(Ljava/lang/String;I)V
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->transceive(Ljava/lang/String;[B)Landroid/os/Bundle;
-Landroid/nfc/INfcAdapterExtras$Stub;-><init>()V
-Landroid/nfc/INfcAdapterExtras$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcAdapterExtras;
-Landroid/nfc/INfcAdapterExtras$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_authenticate:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_close:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_getCardEmulationRoute:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_getDriverName:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_open:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_setCardEmulationRoute:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_transceive:I
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup;
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getServices(ILjava/lang/String;)Ljava/util/List;
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->isDefaultServiceForAid(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->isDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->registerAidGroupForService(ILandroid/content/ComponentName;Landroid/nfc/cardemulation/AidGroup;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->removeAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setDefaultForNextTap(ILandroid/content/ComponentName;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setPreferredService(Landroid/content/ComponentName;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->supportsAidPrefixRegistration()Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->unsetPreferredService()Z
-Landroid/nfc/INfcCardEmulation$Stub;-><init>()V
-Landroid/nfc/INfcCardEmulation$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/INfcCardEmulation$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_getAidGroupForService:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_getServices:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_isDefaultServiceForAid:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_isDefaultServiceForCategory:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_registerAidGroupForService:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_removeAidGroupForService:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setDefaultForNextTap:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setDefaultServiceForCategory:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setPreferredService:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_supportsAidPrefixRegistration:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_unsetPreferredService:I
-Landroid/nfc/INfcCardEmulation;->getAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup;
-Landroid/nfc/INfcCardEmulation;->getServices(ILjava/lang/String;)Ljava/util/List;
-Landroid/nfc/INfcCardEmulation;->isDefaultServiceForAid(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation;->isDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation;->registerAidGroupForService(ILandroid/content/ComponentName;Landroid/nfc/cardemulation/AidGroup;)Z
-Landroid/nfc/INfcCardEmulation;->removeAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation;->setDefaultForNextTap(ILandroid/content/ComponentName;)Z
-Landroid/nfc/INfcCardEmulation;->setDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation;->setPreferredService(Landroid/content/ComponentName;)Z
-Landroid/nfc/INfcCardEmulation;->supportsAidPrefixRegistration()Z
-Landroid/nfc/INfcCardEmulation;->unsetPreferredService()Z
-Landroid/nfc/INfcDta$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcDta$Stub$Proxy;->disableClient()V
-Landroid/nfc/INfcDta$Stub$Proxy;->disableDta()V
-Landroid/nfc/INfcDta$Stub$Proxy;->disableServer()V
-Landroid/nfc/INfcDta$Stub$Proxy;->enableClient(Ljava/lang/String;III)Z
-Landroid/nfc/INfcDta$Stub$Proxy;->enableDta()V
-Landroid/nfc/INfcDta$Stub$Proxy;->enableServer(Ljava/lang/String;IIII)Z
-Landroid/nfc/INfcDta$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcDta$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcDta$Stub$Proxy;->registerMessageService(Ljava/lang/String;)Z
-Landroid/nfc/INfcDta$Stub;-><init>()V
-Landroid/nfc/INfcDta$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcDta;
-Landroid/nfc/INfcDta$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableClient:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableDta:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableServer:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableClient:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableDta:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableServer:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_registerMessageService:I
-Landroid/nfc/INfcDta;->disableClient()V
-Landroid/nfc/INfcDta;->disableDta()V
-Landroid/nfc/INfcDta;->disableServer()V
-Landroid/nfc/INfcDta;->enableClient(Ljava/lang/String;III)Z
-Landroid/nfc/INfcDta;->enableDta()V
-Landroid/nfc/INfcDta;->enableServer(Ljava/lang/String;IIII)Z
-Landroid/nfc/INfcDta;->registerMessageService(Ljava/lang/String;)Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->disableNfcFForegroundService()Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->enableNfcFForegroundService(Landroid/content/ComponentName;)Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getMaxNumOfRegisterableSystemCodes()I
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getNfcFServices(I)Ljava/util/List;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getNfcid2ForService(ILandroid/content/ComponentName;)Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getSystemCodeForService(ILandroid/content/ComponentName;)Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->registerSystemCodeForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->removeSystemCodeForService(ILandroid/content/ComponentName;)Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->setNfcid2ForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcFCardEmulation$Stub;-><init>()V
-Landroid/nfc/INfcFCardEmulation$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/INfcFCardEmulation$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_disableNfcFForegroundService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_enableNfcFForegroundService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getMaxNumOfRegisterableSystemCodes:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getNfcFServices:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getNfcid2ForService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getSystemCodeForService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_registerSystemCodeForService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_removeSystemCodeForService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_setNfcid2ForService:I
-Landroid/nfc/INfcFCardEmulation;->disableNfcFForegroundService()Z
-Landroid/nfc/INfcFCardEmulation;->enableNfcFForegroundService(Landroid/content/ComponentName;)Z
-Landroid/nfc/INfcFCardEmulation;->getMaxNumOfRegisterableSystemCodes()I
-Landroid/nfc/INfcFCardEmulation;->getNfcFServices(I)Ljava/util/List;
-Landroid/nfc/INfcFCardEmulation;->getNfcid2ForService(ILandroid/content/ComponentName;)Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation;->getSystemCodeForService(ILandroid/content/ComponentName;)Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation;->registerSystemCodeForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcFCardEmulation;->removeSystemCodeForService(ILandroid/content/ComponentName;)Z
-Landroid/nfc/INfcFCardEmulation;->setNfcid2ForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcTag$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcTag$Stub$Proxy;->canMakeReadOnly(I)Z
-Landroid/nfc/INfcTag$Stub$Proxy;->connect(II)I
-Landroid/nfc/INfcTag$Stub$Proxy;->formatNdef(I[B)I
-Landroid/nfc/INfcTag$Stub$Proxy;->getExtendedLengthApdusSupported()Z
-Landroid/nfc/INfcTag$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcTag$Stub$Proxy;->getMaxTransceiveLength(I)I
-Landroid/nfc/INfcTag$Stub$Proxy;->getTechList(I)[I
-Landroid/nfc/INfcTag$Stub$Proxy;->getTimeout(I)I
-Landroid/nfc/INfcTag$Stub$Proxy;->isNdef(I)Z
-Landroid/nfc/INfcTag$Stub$Proxy;->isPresent(I)Z
-Landroid/nfc/INfcTag$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcTag$Stub$Proxy;->ndefIsWritable(I)Z
-Landroid/nfc/INfcTag$Stub$Proxy;->ndefMakeReadOnly(I)I
-Landroid/nfc/INfcTag$Stub$Proxy;->ndefRead(I)Landroid/nfc/NdefMessage;
-Landroid/nfc/INfcTag$Stub$Proxy;->ndefWrite(ILandroid/nfc/NdefMessage;)I
-Landroid/nfc/INfcTag$Stub$Proxy;->reconnect(I)I
-Landroid/nfc/INfcTag$Stub$Proxy;->rediscover(I)Landroid/nfc/Tag;
-Landroid/nfc/INfcTag$Stub$Proxy;->resetTimeouts()V
-Landroid/nfc/INfcTag$Stub$Proxy;->setTimeout(II)I
-Landroid/nfc/INfcTag$Stub$Proxy;->transceive(I[BZ)Landroid/nfc/TransceiveResult;
-Landroid/nfc/INfcTag$Stub;-><init>()V
-Landroid/nfc/INfcTag$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcTag;
-Landroid/nfc/INfcTag$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_canMakeReadOnly:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_connect:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_formatNdef:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_getExtendedLengthApdusSupported:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_getMaxTransceiveLength:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_getTechList:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_getTimeout:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_isNdef:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_isPresent:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefIsWritable:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefMakeReadOnly:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefRead:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefWrite:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_reconnect:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_rediscover:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_resetTimeouts:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_setTimeout:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_transceive:I
-Landroid/nfc/INfcTag;->canMakeReadOnly(I)Z
-Landroid/nfc/INfcTag;->connect(II)I
-Landroid/nfc/INfcTag;->formatNdef(I[B)I
-Landroid/nfc/INfcTag;->getExtendedLengthApdusSupported()Z
-Landroid/nfc/INfcTag;->getMaxTransceiveLength(I)I
-Landroid/nfc/INfcTag;->getTechList(I)[I
-Landroid/nfc/INfcTag;->getTimeout(I)I
-Landroid/nfc/INfcTag;->isNdef(I)Z
-Landroid/nfc/INfcTag;->isPresent(I)Z
-Landroid/nfc/INfcTag;->ndefIsWritable(I)Z
-Landroid/nfc/INfcTag;->ndefMakeReadOnly(I)I
-Landroid/nfc/INfcTag;->ndefRead(I)Landroid/nfc/NdefMessage;
-Landroid/nfc/INfcTag;->ndefWrite(ILandroid/nfc/NdefMessage;)I
-Landroid/nfc/INfcTag;->reconnect(I)I
-Landroid/nfc/INfcTag;->rediscover(I)Landroid/nfc/Tag;
-Landroid/nfc/INfcTag;->resetTimeouts()V
-Landroid/nfc/INfcTag;->setTimeout(II)I
-Landroid/nfc/INfcTag;->transceive(I[BZ)Landroid/nfc/TransceiveResult;
-Landroid/nfc/INfcUnlockHandler$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->onUnlockAttempted(Landroid/nfc/Tag;)Z
-Landroid/nfc/INfcUnlockHandler$Stub;-><init>()V
-Landroid/nfc/INfcUnlockHandler$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcUnlockHandler;
-Landroid/nfc/INfcUnlockHandler$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcUnlockHandler$Stub;->TRANSACTION_onUnlockAttempted:I
-Landroid/nfc/INfcUnlockHandler;->onUnlockAttempted(Landroid/nfc/Tag;)Z
-Landroid/nfc/ITagRemovedCallback$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->onTagRemoved()V
-Landroid/nfc/ITagRemovedCallback$Stub;-><init>()V
-Landroid/nfc/ITagRemovedCallback$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/ITagRemovedCallback;
-Landroid/nfc/ITagRemovedCallback$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/ITagRemovedCallback$Stub;->TRANSACTION_onTagRemoved:I
-Landroid/nfc/ITagRemovedCallback;->onTagRemoved()V
-Landroid/nfc/NdefMessage;->mRecords:[Landroid/nfc/NdefRecord;
-Landroid/nfc/NdefRecord;->bytesToString([B)Ljava/lang/StringBuilder;
-Landroid/nfc/NdefRecord;->EMPTY_BYTE_ARRAY:[B
-Landroid/nfc/NdefRecord;->ensureSanePayloadSize(J)V
-Landroid/nfc/NdefRecord;->FLAG_CF:B
-Landroid/nfc/NdefRecord;->FLAG_IL:B
-Landroid/nfc/NdefRecord;->FLAG_MB:B
-Landroid/nfc/NdefRecord;->FLAG_ME:B
-Landroid/nfc/NdefRecord;->FLAG_SR:B
-Landroid/nfc/NdefRecord;->getByteLength()I
-Landroid/nfc/NdefRecord;->MAX_PAYLOAD_SIZE:I
-Landroid/nfc/NdefRecord;->mPayload:[B
-Landroid/nfc/NdefRecord;->mTnf:S
-Landroid/nfc/NdefRecord;->mType:[B
-Landroid/nfc/NdefRecord;->parse(Ljava/nio/ByteBuffer;Z)[Landroid/nfc/NdefRecord;
-Landroid/nfc/NdefRecord;->parseWktUri()Landroid/net/Uri;
-Landroid/nfc/NdefRecord;->RTD_ANDROID_APP:[B
-Landroid/nfc/NdefRecord;->TNF_RESERVED:S
-Landroid/nfc/NdefRecord;->toUri(Z)Landroid/net/Uri;
-Landroid/nfc/NdefRecord;->URI_PREFIX_MAP:[Ljava/lang/String;
-Landroid/nfc/NdefRecord;->validateTnf(S[B[B[B)Ljava/lang/String;
-Landroid/nfc/NdefRecord;->writeToByteBuffer(Ljava/nio/ByteBuffer;ZZ)V
-Landroid/nfc/NfcActivityManager$NfcActivityState;->activity:Landroid/app/Activity;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->destroy()V
-Landroid/nfc/NfcActivityManager$NfcActivityState;->flags:I
-Landroid/nfc/NfcActivityManager$NfcActivityState;->ndefMessage:Landroid/nfc/NdefMessage;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->ndefMessageCallback:Landroid/nfc/NfcAdapter$CreateNdefMessageCallback;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->onNdefPushCompleteCallback:Landroid/nfc/NfcAdapter$OnNdefPushCompleteCallback;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->readerCallback:Landroid/nfc/NfcAdapter$ReaderCallback;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->readerModeExtras:Landroid/os/Bundle;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->readerModeFlags:I
-Landroid/nfc/NfcActivityManager$NfcActivityState;->resumed:Z
-Landroid/nfc/NfcActivityManager$NfcActivityState;->token:Landroid/os/Binder;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->uriCallback:Landroid/nfc/NfcAdapter$CreateBeamUrisCallback;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->uris:[Landroid/net/Uri;
-Landroid/nfc/NfcActivityManager$NfcApplicationState;->app:Landroid/app/Application;
-Landroid/nfc/NfcActivityManager$NfcApplicationState;->refCount:I
-Landroid/nfc/NfcActivityManager$NfcApplicationState;->register()V
-Landroid/nfc/NfcActivityManager$NfcApplicationState;->unregister()V
-Landroid/nfc/NfcActivityManager;-><init>(Landroid/nfc/NfcAdapter;)V
-Landroid/nfc/NfcActivityManager;->createBeamShareData(B)Landroid/nfc/BeamShareData;
-Landroid/nfc/NfcActivityManager;->DBG:Ljava/lang/Boolean;
-Landroid/nfc/NfcActivityManager;->destroyActivityState(Landroid/app/Activity;)V
-Landroid/nfc/NfcActivityManager;->disableReaderMode(Landroid/app/Activity;)V
-Landroid/nfc/NfcActivityManager;->enableReaderMode(Landroid/app/Activity;Landroid/nfc/NfcAdapter$ReaderCallback;ILandroid/os/Bundle;)V
-Landroid/nfc/NfcActivityManager;->findActivityState(Landroid/app/Activity;)Landroid/nfc/NfcActivityManager$NfcActivityState;
-Landroid/nfc/NfcActivityManager;->findAppState(Landroid/app/Application;)Landroid/nfc/NfcActivityManager$NfcApplicationState;
-Landroid/nfc/NfcActivityManager;->findResumedActivityState()Landroid/nfc/NfcActivityManager$NfcActivityState;
-Landroid/nfc/NfcActivityManager;->getActivityState(Landroid/app/Activity;)Landroid/nfc/NfcActivityManager$NfcActivityState;
-Landroid/nfc/NfcActivityManager;->mActivities:Ljava/util/List;
-Landroid/nfc/NfcActivityManager;->mApps:Ljava/util/List;
-Landroid/nfc/NfcActivityManager;->onNdefPushComplete(B)V
-Landroid/nfc/NfcActivityManager;->onTagDiscovered(Landroid/nfc/Tag;)V
-Landroid/nfc/NfcActivityManager;->registerApplication(Landroid/app/Application;)V
-Landroid/nfc/NfcActivityManager;->requestNfcServiceCallback()V
-Landroid/nfc/NfcActivityManager;->setNdefPushContentUri(Landroid/app/Activity;[Landroid/net/Uri;)V
-Landroid/nfc/NfcActivityManager;->setNdefPushContentUriCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$CreateBeamUrisCallback;)V
-Landroid/nfc/NfcActivityManager;->setNdefPushMessage(Landroid/app/Activity;Landroid/nfc/NdefMessage;I)V
-Landroid/nfc/NfcActivityManager;->setNdefPushMessageCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$CreateNdefMessageCallback;I)V
-Landroid/nfc/NfcActivityManager;->setOnNdefPushCompleteCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$OnNdefPushCompleteCallback;)V
-Landroid/nfc/NfcActivityManager;->setReaderMode(Landroid/os/Binder;ILandroid/os/Bundle;)V
-Landroid/nfc/NfcActivityManager;->TAG:Ljava/lang/String;
-Landroid/nfc/NfcActivityManager;->unregisterApplication(Landroid/app/Application;)V
-Landroid/nfc/NfcActivityManager;->verifyNfcPermission()V
-Landroid/nfc/NfcAdapter;-><init>(Landroid/content/Context;)V
-Landroid/nfc/NfcAdapter;->ACTION_HANDOVER_TRANSFER_DONE:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->ACTION_HANDOVER_TRANSFER_STARTED:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->ACTION_TAG_LEFT_FIELD:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->disableForegroundDispatchInternal(Landroid/app/Activity;Z)V
-Landroid/nfc/NfcAdapter;->dispatch(Landroid/nfc/Tag;)V
-Landroid/nfc/NfcAdapter;->enforceResumed(Landroid/app/Activity;)V
-Landroid/nfc/NfcAdapter;->EXTRA_HANDOVER_TRANSFER_STATUS:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->EXTRA_HANDOVER_TRANSFER_URI:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->getCardEmulationService()Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/NfcAdapter;->getNfcDtaInterface()Landroid/nfc/INfcDta;
-Landroid/nfc/NfcAdapter;->getNfcFCardEmulationService()Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/NfcAdapter;->getSdkVersion()I
-Landroid/nfc/NfcAdapter;->getServiceInterface()Landroid/nfc/INfcAdapter;
-Landroid/nfc/NfcAdapter;->getTagService()Landroid/nfc/INfcTag;
-Landroid/nfc/NfcAdapter;->HANDOVER_TRANSFER_STATUS_FAILURE:I
-Landroid/nfc/NfcAdapter;->HANDOVER_TRANSFER_STATUS_SUCCESS:I
-Landroid/nfc/NfcAdapter;->hasNfcFeature()Z
-Landroid/nfc/NfcAdapter;->hasNfcHceFeature()Z
-Landroid/nfc/NfcAdapter;->invokeBeam(Landroid/nfc/BeamShareData;)Z
-Landroid/nfc/NfcAdapter;->mContext:Landroid/content/Context;
-Landroid/nfc/NfcAdapter;->mForegroundDispatchListener:Landroid/app/OnActivityPausedListener;
-Landroid/nfc/NfcAdapter;->mLock:Ljava/lang/Object;
-Landroid/nfc/NfcAdapter;->mNfcActivityManager:Landroid/nfc/NfcActivityManager;
-Landroid/nfc/NfcAdapter;->mNfcUnlockHandlers:Ljava/util/HashMap;
-Landroid/nfc/NfcAdapter;->mTagRemovedListener:Landroid/nfc/ITagRemovedCallback;
-Landroid/nfc/NfcAdapter;->pausePolling(I)V
-Landroid/nfc/NfcAdapter;->resumePolling()V
-Landroid/nfc/NfcAdapter;->sCardEmulationService:Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/NfcAdapter;->setP2pModes(II)V
-Landroid/nfc/NfcAdapter;->sHasNfcFeature:Z
-Landroid/nfc/NfcAdapter;->sIsInitialized:Z
-Landroid/nfc/NfcAdapter;->sNfcAdapters:Ljava/util/HashMap;
-Landroid/nfc/NfcAdapter;->sNfcFCardEmulationService:Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/NfcAdapter;->sNullContextNfcAdapter:Landroid/nfc/NfcAdapter;
-Landroid/nfc/NfcAdapter;->sTagService:Landroid/nfc/INfcTag;
-Landroid/nfc/NfcAdapter;->TAG:Ljava/lang/String;
-Landroid/nfc/NfcEvent;-><init>(Landroid/nfc/NfcAdapter;B)V
-Landroid/nfc/NfcManager;->mAdapter:Landroid/nfc/NfcAdapter;
-Landroid/nfc/Tag;-><init>([B[I[Landroid/os/Bundle;ILandroid/nfc/INfcTag;)V
-Landroid/nfc/Tag;->createMockTag([B[I[Landroid/os/Bundle;)Landroid/nfc/Tag;
-Landroid/nfc/Tag;->generateTechStringList([I)[Ljava/lang/String;
-Landroid/nfc/Tag;->getConnectedTechnology()I
-Landroid/nfc/Tag;->getTechCodeList()[I
-Landroid/nfc/Tag;->getTechCodesFromStrings([Ljava/lang/String;)[I
-Landroid/nfc/Tag;->getTechExtras(I)Landroid/os/Bundle;
-Landroid/nfc/Tag;->getTechStringToCodeMap()Ljava/util/HashMap;
-Landroid/nfc/Tag;->hasTech(I)Z
-Landroid/nfc/Tag;->mConnectedTechnology:I
-Landroid/nfc/Tag;->mServiceHandle:I
-Landroid/nfc/Tag;->mTagService:Landroid/nfc/INfcTag;
-Landroid/nfc/Tag;->mTechExtras:[Landroid/os/Bundle;
-Landroid/nfc/Tag;->mTechList:[I
-Landroid/nfc/Tag;->mTechStringList:[Ljava/lang/String;
-Landroid/nfc/Tag;->readBytesWithNull(Landroid/os/Parcel;)[B
-Landroid/nfc/Tag;->rediscover()Landroid/nfc/Tag;
-Landroid/nfc/Tag;->setConnectedTechnology(I)V
-Landroid/nfc/Tag;->setTechnologyDisconnected()V
-Landroid/nfc/Tag;->writeBytesWithNull(Landroid/os/Parcel;[B)V
-Landroid/nfc/tech/BasicTagTechnology;-><init>(Landroid/nfc/Tag;I)V
-Landroid/nfc/tech/BasicTagTechnology;->checkConnected()V
-Landroid/nfc/tech/BasicTagTechnology;->getMaxTransceiveLengthInternal()I
-Landroid/nfc/tech/BasicTagTechnology;->mIsConnected:Z
-Landroid/nfc/tech/BasicTagTechnology;->mSelectedTechnology:I
-Landroid/nfc/tech/BasicTagTechnology;->mTag:Landroid/nfc/Tag;
-Landroid/nfc/tech/BasicTagTechnology;->reconnect()V
-Landroid/nfc/tech/BasicTagTechnology;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/BasicTagTechnology;->transceive([BZ)[B
-Landroid/nfc/tech/IsoDep;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/IsoDep;->EXTRA_HIST_BYTES:Ljava/lang/String;
-Landroid/nfc/tech/IsoDep;->EXTRA_HI_LAYER_RESP:Ljava/lang/String;
-Landroid/nfc/tech/IsoDep;->mHiLayerResponse:[B
-Landroid/nfc/tech/IsoDep;->mHistBytes:[B
-Landroid/nfc/tech/IsoDep;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/MifareClassic;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/MifareClassic;->authenticate(I[BZ)Z
-Landroid/nfc/tech/MifareClassic;->isEmulated()Z
-Landroid/nfc/tech/MifareClassic;->MAX_BLOCK_COUNT:I
-Landroid/nfc/tech/MifareClassic;->MAX_SECTOR_COUNT:I
-Landroid/nfc/tech/MifareClassic;->mIsEmulated:Z
-Landroid/nfc/tech/MifareClassic;->mSize:I
-Landroid/nfc/tech/MifareClassic;->mType:I
-Landroid/nfc/tech/MifareClassic;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/MifareClassic;->validateBlock(I)V
-Landroid/nfc/tech/MifareClassic;->validateSector(I)V
-Landroid/nfc/tech/MifareClassic;->validateValueOperand(I)V
-Landroid/nfc/tech/MifareUltralight;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/MifareUltralight;->EXTRA_IS_UL_C:Ljava/lang/String;
-Landroid/nfc/tech/MifareUltralight;->MAX_PAGE_COUNT:I
-Landroid/nfc/tech/MifareUltralight;->mType:I
-Landroid/nfc/tech/MifareUltralight;->NXP_MANUFACTURER_ID:I
-Landroid/nfc/tech/MifareUltralight;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/MifareUltralight;->validatePageIndex(I)V
-Landroid/nfc/tech/Ndef;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/Ndef;->EXTRA_NDEF_CARDSTATE:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->EXTRA_NDEF_MAXLENGTH:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->EXTRA_NDEF_MSG:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->EXTRA_NDEF_TYPE:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->ICODE_SLI:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->mCardState:I
-Landroid/nfc/tech/Ndef;->mMaxNdefSize:I
-Landroid/nfc/tech/Ndef;->mNdefMsg:Landroid/nfc/NdefMessage;
-Landroid/nfc/tech/Ndef;->mNdefType:I
-Landroid/nfc/tech/Ndef;->NDEF_MODE_READ_ONLY:I
-Landroid/nfc/tech/Ndef;->NDEF_MODE_READ_WRITE:I
-Landroid/nfc/tech/Ndef;->NDEF_MODE_UNKNOWN:I
-Landroid/nfc/tech/Ndef;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->TYPE_1:I
-Landroid/nfc/tech/Ndef;->TYPE_2:I
-Landroid/nfc/tech/Ndef;->TYPE_3:I
-Landroid/nfc/tech/Ndef;->TYPE_4:I
-Landroid/nfc/tech/Ndef;->TYPE_ICODE_SLI:I
-Landroid/nfc/tech/Ndef;->TYPE_MIFARE_CLASSIC:I
-Landroid/nfc/tech/Ndef;->TYPE_OTHER:I
-Landroid/nfc/tech/Ndef;->UNKNOWN:Ljava/lang/String;
-Landroid/nfc/tech/NdefFormatable;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NdefFormatable;->format(Landroid/nfc/NdefMessage;Z)V
-Landroid/nfc/tech/NdefFormatable;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/NfcA;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcA;->EXTRA_ATQA:Ljava/lang/String;
-Landroid/nfc/tech/NfcA;->EXTRA_SAK:Ljava/lang/String;
-Landroid/nfc/tech/NfcA;->mAtqa:[B
-Landroid/nfc/tech/NfcA;->mSak:S
-Landroid/nfc/tech/NfcA;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/NfcB;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcB;->EXTRA_APPDATA:Ljava/lang/String;
-Landroid/nfc/tech/NfcB;->EXTRA_PROTINFO:Ljava/lang/String;
-Landroid/nfc/tech/NfcB;->mAppData:[B
-Landroid/nfc/tech/NfcB;->mProtInfo:[B
-Landroid/nfc/tech/NfcBarcode;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcBarcode;->EXTRA_BARCODE_TYPE:Ljava/lang/String;
-Landroid/nfc/tech/NfcBarcode;->mType:I
-Landroid/nfc/tech/NfcF;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcF;->EXTRA_PMM:Ljava/lang/String;
-Landroid/nfc/tech/NfcF;->EXTRA_SC:Ljava/lang/String;
-Landroid/nfc/tech/NfcF;->mManufacturer:[B
-Landroid/nfc/tech/NfcF;->mSystemCode:[B
-Landroid/nfc/tech/NfcF;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/NfcV;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcV;->EXTRA_DSFID:Ljava/lang/String;
-Landroid/nfc/tech/NfcV;->EXTRA_RESP_FLAGS:Ljava/lang/String;
-Landroid/nfc/tech/NfcV;->mDsfId:B
-Landroid/nfc/tech/NfcV;->mRespFlags:B
-Landroid/nfc/tech/TagTechnology;->ISO_DEP:I
-Landroid/nfc/tech/TagTechnology;->MIFARE_CLASSIC:I
-Landroid/nfc/tech/TagTechnology;->MIFARE_ULTRALIGHT:I
-Landroid/nfc/tech/TagTechnology;->NDEF:I
-Landroid/nfc/tech/TagTechnology;->NDEF_FORMATABLE:I
-Landroid/nfc/tech/TagTechnology;->NFC_A:I
-Landroid/nfc/tech/TagTechnology;->NFC_B:I
-Landroid/nfc/tech/TagTechnology;->NFC_BARCODE:I
-Landroid/nfc/tech/TagTechnology;->NFC_F:I
-Landroid/nfc/tech/TagTechnology;->NFC_V:I
-Landroid/nfc/tech/TagTechnology;->reconnect()V
-Landroid/nfc/TechListParcel;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/TechListParcel;->getTechLists()[[Ljava/lang/String;
-Landroid/nfc/TechListParcel;->mTechLists:[[Ljava/lang/String;
-Landroid/nfc/TransceiveResult;-><init>(I[B)V
-Landroid/nfc/TransceiveResult;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/TransceiveResult;->getResponseOrThrow()[B
-Landroid/nfc/TransceiveResult;->mResponseData:[B
-Landroid/nfc/TransceiveResult;->mResult:I
-Landroid/nfc/TransceiveResult;->RESULT_EXCEEDED_LENGTH:I
-Landroid/nfc/TransceiveResult;->RESULT_FAILURE:I
-Landroid/nfc/TransceiveResult;->RESULT_SUCCESS:I
-Landroid/nfc/TransceiveResult;->RESULT_TAGLOST:I
 Landroid/opengl/EGL14;->eglCreatePbufferFromClientBuffer(Landroid/opengl/EGLDisplay;IJLandroid/opengl/EGLConfig;[II)Landroid/opengl/EGLSurface;
 Landroid/opengl/EGL14;->_eglCreateWindowSurface(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLConfig;Ljava/lang/Object;[II)Landroid/opengl/EGLSurface;
 Landroid/opengl/EGL14;->_eglCreateWindowSurfaceTexture(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLConfig;Ljava/lang/Object;[II)Landroid/opengl/EGLSurface;
diff --git a/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt b/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt
index f5184e7..4df1dca 100644
--- a/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt
+++ b/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt
@@ -19,7 +19,6 @@
 Landroid/media/IVolumeController$Stub;->asInterface(Landroid/os/IBinder;)Landroid/media/IVolumeController;
 Landroid/net/INetworkPolicyListener$Stub;-><init>()V
 Landroid/net/sip/ISipSession$Stub;->asInterface(Landroid/os/IBinder;)Landroid/net/sip/ISipSession;
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_enable:I
 Landroid/os/IPowerManager$Stub;->TRANSACTION_acquireWakeLock:I
 Landroid/os/IPowerManager$Stub;->TRANSACTION_goToSleep:I
 Landroid/service/euicc/IEuiccService$Stub;-><init>()V
diff --git a/core/api/current.txt b/core/api/current.txt
index 8a1a466..15c054f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1602,7 +1602,6 @@
     field public static final int switchTextOff = 16843628; // 0x101036c
     field public static final int switchTextOn = 16843627; // 0x101036b
     field public static final int syncable = 16842777; // 0x1010019
-    field @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") public static final int systemUserOnly;
     field public static final int tabStripEnabled = 16843453; // 0x10102bd
     field public static final int tabStripLeft = 16843451; // 0x10102bb
     field public static final int tabStripRight = 16843452; // 0x10102bc
@@ -4625,9 +4624,9 @@
 
   public class ActivityManager {
     method public int addAppTask(@NonNull android.app.Activity, @NonNull android.content.Intent, @Nullable android.app.ActivityManager.TaskDescription, @NonNull android.graphics.Bitmap);
+    method @FlaggedApi("android.app.app_start_info") public void addApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
     method @FlaggedApi("android.app.app_start_info") public void addStartInfoTimestamp(@IntRange(from=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START, to=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER) int, long);
     method public void appNotResponding(@NonNull String);
-    method @FlaggedApi("android.app.app_start_info") public void clearApplicationStartInfoCompletionListener();
     method public boolean clearApplicationUserData();
     method public void clearWatchHeapLimit();
     method @RequiresPermission(android.Manifest.permission.DUMP) public void dumpPackageState(java.io.FileDescriptor, String);
@@ -4661,8 +4660,8 @@
     method @RequiresPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES) public void killBackgroundProcesses(String);
     method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int);
     method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int, android.os.Bundle);
+    method @FlaggedApi("android.app.app_start_info") public void removeApplicationStartInfoCompletionListener(@NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
     method @Deprecated public void restartPackage(String);
-    method @FlaggedApi("android.app.app_start_info") public void setApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
     method public void setProcessStateSummary(@Nullable byte[]);
     method public static void setVrThread(int);
     method public void setWatchHeapLimit(long);
@@ -9486,12 +9485,15 @@
     method @NonNull public java.util.List<android.appwidget.AppWidgetProviderInfo> getInstalledProvidersForPackage(@NonNull String, @Nullable android.os.UserHandle);
     method @NonNull public java.util.List<android.appwidget.AppWidgetProviderInfo> getInstalledProvidersForProfile(@Nullable android.os.UserHandle);
     method public static android.appwidget.AppWidgetManager getInstance(android.content.Context);
+    method @FlaggedApi("android.appwidget.flags.generated_previews") @Nullable public android.widget.RemoteViews getWidgetPreview(@NonNull android.content.ComponentName, @Nullable android.os.UserHandle, int);
     method public boolean isRequestPinAppWidgetSupported();
     method @Deprecated public void notifyAppWidgetViewDataChanged(int[], int);
     method @Deprecated public void notifyAppWidgetViewDataChanged(int, int);
     method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews);
     method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews);
+    method @FlaggedApi("android.appwidget.flags.generated_previews") public void removeWidgetPreview(@NonNull android.content.ComponentName, int);
     method public boolean requestPinAppWidget(@NonNull android.content.ComponentName, @Nullable android.os.Bundle, @Nullable android.app.PendingIntent);
+    method @FlaggedApi("android.appwidget.flags.generated_previews") public void setWidgetPreview(@NonNull android.content.ComponentName, int, @NonNull android.widget.RemoteViews);
     method public void updateAppWidget(int[], android.widget.RemoteViews);
     method public void updateAppWidget(int, android.widget.RemoteViews);
     method public void updateAppWidget(android.content.ComponentName, android.widget.RemoteViews);
@@ -9565,6 +9567,7 @@
     field public int autoAdvanceViewId;
     field public android.content.ComponentName configure;
     field @IdRes public int descriptionRes;
+    field @FlaggedApi("android.appwidget.flags.generated_previews") public int generatedPreviewCategories;
     field public int icon;
     field public int initialKeyguardLayout;
     field public int initialLayout;
@@ -10383,6 +10386,7 @@
     method @CheckResult(suggest="#enforceCallingPermission(String,String)") public abstract int checkCallingPermission(@NonNull String);
     method @CheckResult(suggest="#enforceCallingUriPermission(Uri,int,String)") public abstract int checkCallingUriPermission(android.net.Uri, int);
     method @NonNull public int[] checkCallingUriPermissions(@NonNull java.util.List<android.net.Uri>, int);
+    method @FlaggedApi("android.security.content_uri_permission_apis") public int checkContentUriPermissionFull(@NonNull android.net.Uri, int, int, int);
     method @CheckResult(suggest="#enforcePermission(String,int,int,String)") public abstract int checkPermission(@NonNull String, int, int);
     method public abstract int checkSelfPermission(@NonNull String);
     method @CheckResult(suggest="#enforceUriPermission(Uri,int,int,String)") public abstract int checkUriPermission(android.net.Uri, int, int, int);
@@ -10541,6 +10545,7 @@
     field public static final int BIND_INCLUDE_CAPABILITIES = 4096; // 0x1000
     field public static final int BIND_NOT_FOREGROUND = 4; // 0x4
     field public static final int BIND_NOT_PERCEPTIBLE = 256; // 0x100
+    field @FlaggedApi("android.content.flags.enable_bind_package_isolated_process") public static final int BIND_PACKAGE_ISOLATED_PROCESS = 16384; // 0x4000
     field public static final int BIND_SHARED_ISOLATED_PROCESS = 8192; // 0x2000
     field public static final int BIND_WAIVE_PRIORITY = 32; // 0x20
     field public static final String BIOMETRIC_SERVICE = "biometric";
@@ -18676,6 +18681,7 @@
     method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public void authenticate(@NonNull android.hardware.biometrics.BiometricPrompt.CryptoObject, @NonNull android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
     method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public void authenticate(@NonNull android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
     method @Nullable public int getAllowedAuthenticators();
+    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView();
     method @Nullable public CharSequence getDescription();
     method @Nullable public CharSequence getNegativeButtonText();
     method @Nullable public CharSequence getSubtitle();
@@ -18723,6 +18729,7 @@
     method @NonNull public android.hardware.biometrics.BiometricPrompt build();
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setAllowedAuthenticators(int);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setConfirmationRequired(boolean);
+    method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence);
     method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean);
     method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener);
@@ -18746,6 +18753,43 @@
     method @Nullable public java.security.Signature getSignature();
   }
 
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentListItem {
+  }
+
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem {
+    ctor public PromptContentListItemBulletedText(@NonNull CharSequence);
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemBulletedText> CREATOR;
+  }
+
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem {
+    ctor public PromptContentListItemPlainText(@NonNull CharSequence);
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemPlainText> CREATOR;
+  }
+
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentView {
+  }
+
+  @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
+    method public int describeContents();
+    method @Nullable public CharSequence getDescription();
+    method @NonNull public java.util.List<android.hardware.biometrics.PromptContentListItem> getListItems();
+    method public static int getMaxEachItemCharacterNumber();
+    method public static int getMaxItemCount();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptVerticalListContentView> CREATOR;
+  }
+
+  public static final class PromptVerticalListContentView.Builder {
+    ctor public PromptVerticalListContentView.Builder();
+    method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentListItem);
+    method @NonNull public android.hardware.biometrics.PromptVerticalListContentView build();
+    method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull CharSequence);
+  }
+
 }
 
 package android.hardware.camera2 {
@@ -40468,7 +40512,6 @@
 
   public final class ZenPolicy implements android.os.Parcelable {
     method public int describeContents();
-    method @FlaggedApi("android.app.modes_api") public int getAllowedChannels();
     method public int getPriorityCallSenders();
     method public int getPriorityCategoryAlarms();
     method public int getPriorityCategoryCalls();
@@ -40479,6 +40522,7 @@
     method public int getPriorityCategoryReminders();
     method public int getPriorityCategoryRepeatCallers();
     method public int getPriorityCategorySystem();
+    method @FlaggedApi("android.app.modes_api") public int getPriorityChannels();
     method public int getPriorityConversationSenders();
     method public int getPriorityMessageSenders();
     method public int getVisualEffectAmbient();
@@ -40489,9 +40533,6 @@
     method public int getVisualEffectPeek();
     method public int getVisualEffectStatusBar();
     method public void writeToParcel(android.os.Parcel, int);
-    field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_NONE = 2; // 0x2
-    field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_PRIORITY = 1; // 0x1
-    field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_UNSET = 0; // 0x0
     field public static final int CONVERSATION_SENDERS_ANYONE = 1; // 0x1
     field public static final int CONVERSATION_SENDERS_IMPORTANT = 2; // 0x2
     field public static final int CONVERSATION_SENDERS_NONE = 3; // 0x3
@@ -40512,11 +40553,11 @@
     method @NonNull public android.service.notification.ZenPolicy.Builder allowAlarms(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowAllSounds();
     method @NonNull public android.service.notification.ZenPolicy.Builder allowCalls(int);
-    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowChannels(int);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowConversations(int);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowEvents(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowMedia(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowMessages(int);
+    method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowReminders(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowRepeatCallers(boolean);
     method @NonNull public android.service.notification.ZenPolicy.Builder allowSystem(boolean);
@@ -41612,6 +41653,7 @@
     method public void sendEvent(@NonNull String, @NonNull android.os.Bundle);
     method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
     method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+    method @FlaggedApi("com.android.server.telecom.flags.set_mute_state") public void setMuteState(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
     method public void startCallStreaming(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
   }
 
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ebd0544..d26662d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3221,6 +3221,7 @@
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
     method @NonNull public android.content.Context createContext();
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
+    method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
     method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
     method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
@@ -3229,6 +3230,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
+    method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method public int getDeviceId();
@@ -3274,6 +3276,7 @@
     field @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
     field @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
     field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
+    field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5
     field @FlaggedApi("android.companion.virtual.flags.cross_device_clipboard") public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4
     field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
     field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
@@ -3356,30 +3359,38 @@
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
     method public default void onProcessCaptureRequest(int, long);
     method public void onStreamClosed(int);
-    method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig);
+    method public void onStreamConfigured(int, @NonNull android.view.Surface, @IntRange(from=1) int, @IntRange(from=1) int, int);
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable {
     method public int describeContents();
+    method public int getLensFacing();
     method @NonNull public String getName();
+    method public int getSensorOrientation();
     method @NonNull public java.util.Set<android.companion.virtual.camera.VirtualCameraStreamConfig> getStreamConfigs();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraConfig> CREATOR;
+    field public static final int SENSOR_ORIENTATION_0 = 0; // 0x0
+    field public static final int SENSOR_ORIENTATION_180 = 180; // 0xb4
+    field public static final int SENSOR_ORIENTATION_270 = 270; // 0x10e
+    field public static final int SENSOR_ORIENTATION_90 = 90; // 0x5a
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder {
     ctor public VirtualCameraConfig.Builder();
-    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(int, int, int);
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int);
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build();
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setLensFacing(int);
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setName(@NonNull String);
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setSensorOrientation(int);
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
-    ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int);
     method public int describeContents();
     method public int getFormat();
     method @IntRange(from=1) public int getHeight();
+    method @IntRange(from=1) public int getMaximumFramesPerSecond();
     method @IntRange(from=1) public int getWidth();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraStreamConfig> CREATOR;
@@ -5304,6 +5315,78 @@
     method @NonNull public android.hardware.input.VirtualNavigationTouchpadConfig build();
   }
 
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public class VirtualStylus implements java.io.Closeable {
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent);
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent);
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusButtonEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public int getButtonCode();
+    method public long getEventTimeNanos();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int ACTION_BUTTON_PRESS = 11; // 0xb
+    field public static final int ACTION_BUTTON_RELEASE = 12; // 0xc
+    field public static final int BUTTON_PRIMARY = 32; // 0x20
+    field public static final int BUTTON_SECONDARY = 64; // 0x40
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusButtonEvent> CREATOR;
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusButtonEvent.Builder {
+    ctor public VirtualStylusButtonEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualStylusButtonEvent build();
+    method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setAction(int);
+    method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setButtonCode(int);
+    method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setEventTimeNanos(long);
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getHeight();
+    method public int getWidth();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusConfig> CREATOR;
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualStylusConfig.Builder> {
+    ctor public VirtualStylusConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int);
+    method @NonNull public android.hardware.input.VirtualStylusConfig build();
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusMotionEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public long getEventTimeNanos();
+    method public int getPressure();
+    method public int getTiltX();
+    method public int getTiltY();
+    method public int getToolType();
+    method public int getX();
+    method public int getY();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int ACTION_DOWN = 0; // 0x0
+    field public static final int ACTION_MOVE = 2; // 0x2
+    field public static final int ACTION_UP = 1; // 0x1
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusMotionEvent> CREATOR;
+    field public static final int TOOL_TYPE_ERASER = 4; // 0x4
+    field public static final int TOOL_TYPE_STYLUS = 2; // 0x2
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusMotionEvent.Builder {
+    ctor public VirtualStylusMotionEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent build();
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setAction(int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setEventTimeNanos(long);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setPressure(@IntRange(from=0x0, to=0xff) int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setTiltX(@IntRange(from=0xffffffa6, to=0x5a) int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setTiltY(@IntRange(from=0xffffffa6, to=0x5a) int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setToolType(int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setX(int);
+    method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setY(int);
+  }
+
   public final class VirtualTouchEvent implements android.os.Parcelable {
     method public int describeContents();
     method public int getAction();
@@ -17023,6 +17106,7 @@
   @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class SatelliteManager {
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addSatelliteAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatelliteService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+    method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getAllSatellitePlmnsForCarrier(int);
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getSatelliteAttachRestrictionReasonsForCarrier(int);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 6b7f4880..1fd49ef 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -3574,7 +3574,7 @@
          * foreground.  This may be running a window that is behind the current
          * foreground (so paused and with its state saved, not interacting with
          * the user, but visible to them to some degree); it may also be running
-         * other services under the system's control that it inconsiders important.
+         * other services under the system's control that it considers important.
          */
         public static final int IMPORTANCE_VISIBLE = 200;
 
@@ -3646,9 +3646,9 @@
         public static final int IMPORTANCE_CANT_SAVE_STATE = 350;
 
         /**
-         * Constant for {@link #importance}: This process process contains
-         * cached code that is expendable, not actively running any app components
-         * we care about.
+         * Constant for {@link #importance}: This process contains cached code
+         * that is expendable, not actively running any app components we care
+         * about.
          */
         public static final int IMPORTANCE_CACHED = 400;
 
@@ -4052,10 +4052,28 @@
         }
     }
 
+    private final ArrayList<AppStartInfoCallbackWrapper> mAppStartInfoCallbacks =
+            new ArrayList<>();
+    @Nullable
+    private IApplicationStartInfoCompleteListener mAppStartInfoCompleteListener = null;
+
+    private static final class AppStartInfoCallbackWrapper {
+        @NonNull final Executor mExecutor;
+        @NonNull final Consumer<ApplicationStartInfo> mListener;
+
+        AppStartInfoCallbackWrapper(@NonNull final Executor executor,
+                @NonNull final Consumer<ApplicationStartInfo> listener) {
+            mExecutor = executor;
+            mListener = listener;
+        }
+    }
+
     /**
-     * Sets a callback to be notified when the {@link ApplicationStartInfo} records of this startup
+     * Adds a callback to be notified when the {@link ApplicationStartInfo} records of this startup
      * are complete.
      *
+     * <p class="note"> Note: callback will be removed automatically after being triggered.</p>
+     *
      * <p class="note"> Note: callback will not wait for {@link Activity#reportFullyDrawn} to occur.
      * Timestamp for fully drawn may be added after callback occurs. Set callback after invoking
      * {@link Activity#reportFullyDrawn} if timestamp for fully drawn is required.</p>
@@ -4073,33 +4091,77 @@
      * @throws IllegalArgumentException if executor or listener are null.
      */
     @FlaggedApi(Flags.FLAG_APP_START_INFO)
-    public void setApplicationStartInfoCompletionListener(@NonNull final Executor executor,
+    public void addApplicationStartInfoCompletionListener(@NonNull final Executor executor,
             @NonNull final Consumer<ApplicationStartInfo> listener) {
         Preconditions.checkNotNull(executor, "executor cannot be null");
         Preconditions.checkNotNull(listener, "listener cannot be null");
-        IApplicationStartInfoCompleteListener callback =
-                new IApplicationStartInfoCompleteListener.Stub() {
-            @Override
-            public void onApplicationStartInfoComplete(ApplicationStartInfo applicationStartInfo) {
-                executor.execute(() -> listener.accept(applicationStartInfo));
+        synchronized (mAppStartInfoCallbacks) {
+            for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+                if (listener.equals(mAppStartInfoCallbacks.get(i).mListener)) {
+                    return;
+                }
             }
-        };
-        try {
-            getService().setApplicationStartInfoCompleteListener(callback, mContext.getUserId());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            if (mAppStartInfoCompleteListener == null) {
+                mAppStartInfoCompleteListener = new IApplicationStartInfoCompleteListener.Stub() {
+                    @Override
+                    public void onApplicationStartInfoComplete(
+                            ApplicationStartInfo applicationStartInfo) {
+                        synchronized (mAppStartInfoCallbacks) {
+                            for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+                                final AppStartInfoCallbackWrapper callback =
+                                        mAppStartInfoCallbacks.get(i);
+                                callback.mExecutor.execute(() -> callback.mListener.accept(
+                                        applicationStartInfo));
+                            }
+                            mAppStartInfoCallbacks.clear();
+                            mAppStartInfoCompleteListener = null;
+                        }
+                    }
+                };
+                boolean succeeded = false;
+                try {
+                    getService().addApplicationStartInfoCompleteListener(
+                            mAppStartInfoCompleteListener, mContext.getUserId());
+                    succeeded = true;
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+                if (succeeded) {
+                    mAppStartInfoCallbacks.add(new AppStartInfoCallbackWrapper(executor, listener));
+                } else {
+                    mAppStartInfoCompleteListener = null;
+                    mAppStartInfoCallbacks.clear();
+                }
+            } else {
+                mAppStartInfoCallbacks.add(new AppStartInfoCallbackWrapper(executor, listener));
+            }
         }
     }
 
     /**
-     * Removes the callback set by {@link #setApplicationStartInfoCompletionListener} if there is one.
+     * Removes the provided callback set by {@link #addApplicationStartInfoCompletionListener}.
      */
     @FlaggedApi(Flags.FLAG_APP_START_INFO)
-    public void clearApplicationStartInfoCompletionListener() {
-        try {
-            getService().clearApplicationStartInfoCompleteListener(mContext.getUserId());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+    public void removeApplicationStartInfoCompletionListener(
+            @NonNull final Consumer<ApplicationStartInfo> listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        synchronized (mAppStartInfoCallbacks) {
+            for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+                final AppStartInfoCallbackWrapper callback = mAppStartInfoCallbacks.get(i);
+                if (listener.equals(callback.mListener)) {
+                    mAppStartInfoCallbacks.remove(i);
+                    break;
+                }
+            }
+            if (mAppStartInfoCompleteListener != null && mAppStartInfoCallbacks.isEmpty()) {
+                try {
+                    getService().removeApplicationStartInfoCompleteListener(
+                            mAppStartInfoCompleteListener, mContext.getUserId());
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+                mAppStartInfoCompleteListener = null;
+            }
         }
     }
 
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 656feb0..cc3eac1 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -39,12 +39,17 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Iterator;
 import java.util.Map;
-import java.util.Set;
 
 /**
- * Provide information related to a processes startup.
+ * Describes information related to an application process's startup.
+ *
+ * <p>
+ * Many aspects concerning why and how an applications process was started are valuable for apps
+ * both for logging and for potential behavior changes. Reason for process start, start type,
+ * start times, throttling, and other useful diagnostic data can be obtained from
+ * {@link ApplicationStartInfo} records.
+ * </p>
  */
 @FlaggedApi(Flags.FLAG_APP_START_INFO)
 public final class ApplicationStartInfo implements Parcelable {
@@ -552,13 +557,12 @@
         dest.writeInt(mDefiningUid);
         dest.writeString(mProcessName);
         dest.writeInt(mReason);
-        dest.writeInt(mStartupTimestampsNs.size());
-        Set<Map.Entry<Integer, Long>> timestampEntrySet = mStartupTimestampsNs.entrySet();
-        Iterator<Map.Entry<Integer, Long>> iter = timestampEntrySet.iterator();
-        while (iter.hasNext()) {
-            Map.Entry<Integer, Long> entry = iter.next();
-            dest.writeInt(entry.getKey());
-            dest.writeLong(entry.getValue());
+        dest.writeInt(mStartupTimestampsNs == null ? 0 : mStartupTimestampsNs.size());
+        if (mStartupTimestampsNs != null) {
+            for (int i = 0; i < mStartupTimestampsNs.size(); i++) {
+                dest.writeInt(mStartupTimestampsNs.keyAt(i));
+                dest.writeLong(mStartupTimestampsNs.valueAt(i));
+            }
         }
         dest.writeInt(mStartType);
         dest.writeParcelable(mStartIntent, flags);
@@ -740,13 +744,11 @@
             sb.append(" intent=").append(mStartIntent.toString())
                 .append('\n');
         }
-        if (mStartupTimestampsNs.size() > 0) {
+        if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) {
             sb.append(" timestamps: ");
-            Set<Map.Entry<Integer, Long>> timestampEntrySet = mStartupTimestampsNs.entrySet();
-            Iterator<Map.Entry<Integer, Long>> iter = timestampEntrySet.iterator();
-            while (iter.hasNext()) {
-                Map.Entry<Integer, Long> entry = iter.next();
-                sb.append(entry.getKey()).append("=").append(entry.getValue()).append(" ");
+            for (int i = 0; i < mStartupTimestampsNs.size(); i++) {
+                sb.append(mStartupTimestampsNs.keyAt(i)).append("=").append(mStartupTimestampsNs
+                        .valueAt(i)).append(" ");
             }
             sb.append('\n');
         }
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index edeec77..ed00d9c 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -2409,6 +2409,17 @@
         }
     }
 
+    @Override
+    public int checkContentUriPermissionFull(Uri uri, int pid, int uid, int modeFlags) {
+        try {
+            return ActivityManager.getService().checkContentUriPermissionFull(
+                    ContentProvider.getUriWithoutUserId(uri), pid, uid, modeFlags,
+                    resolveUserId(uri));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     @NonNull
     @Override
     public int[] checkUriPermissions(@NonNull List<Uri> uris, int pid, int uid,
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 260e985..b5d88e8 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -274,6 +274,7 @@
     int getProcessLimit();
     int checkUriPermission(in Uri uri, int pid, int uid, int mode, int userId,
             in IBinder callerToken);
+    int checkContentUriPermissionFull(in Uri uri, int pid, int uid, int mode, int userId);
     int[] checkUriPermissions(in List<Uri> uris, int pid, int uid, int mode, int userId,
                 in IBinder callerToken);
     void grantUriPermission(in IApplicationThread caller, in String targetPkg, in Uri uri,
@@ -715,7 +716,7 @@
      * @param listener    A listener to for the callback upon completion of startup data collection.
      * @param userId      The userId in the multi-user environment.
      */
-    void setApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
+    void addApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
             int userId);
 
 
@@ -724,7 +725,8 @@
      *
      * @param userId      The userId in the multi-user environment.
      */
-    void clearApplicationStartInfoCompleteListener(int userId);
+    void removeApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
+            int userId);
 
 
     /**
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index b303ea6..4fc25fd 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -13,3 +13,10 @@
      description: "API to get importance of UID that's binding to the caller"
      bug: "292533010"
 }
+
+flag {
+    namespace: "backstage_power"
+    name: "app_restrictions_api"
+    description: "API to track and query restrictions applied to apps"
+    bug: "320150834"
+}
\ No newline at end of file
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index 4cf9fca..6204edc 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -19,6 +19,7 @@
 import static android.appwidget.flags.Flags.remoteAdapterConversion;
 
 import android.annotation.BroadcastBehavior;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresFeature;
@@ -30,6 +31,7 @@
 import android.annotation.UserIdInt;
 import android.app.IServiceConnection;
 import android.app.PendingIntent;
+import android.appwidget.flags.Flags;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -1415,6 +1417,89 @@
         }
     }
 
+    /**
+     * Set a preview for this widget. This preview will be used instead of the provider's {@link
+     * AppWidgetProviderInfo#previewLayout previewLayout} or {@link
+     * AppWidgetProviderInfo#previewImage previewImage} for previewing the widget in the widget
+     * picker and pin app widget flow.
+     *
+     * @param provider The {@link ComponentName} for the {@link android.content.BroadcastReceiver
+     *    BroadcastReceiver} provider for the AppWidget you intend to provide a preview for.
+     * @param widgetCategories The categories that this preview should be used for. This can be a
+     *    single category or combination of categories. If multiple categories are specified,
+     *    then this preview will be used for each of those categories. For example, if you
+     *    set a preview for WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD, the preview will
+     *    be used when picking widgets for the home screen and keyguard.
+     *
+     *    <p>Note: You should only use the widget categories that the provider supports, as defined
+     *    in {@link AppWidgetProviderInfo#widgetCategory}.
+     * @param preview This preview will be used for previewing the provider when picking widgets for
+     *    the selected categories.
+     *
+     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_HOME_SCREEN
+     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_KEYGUARD
+     * @see AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX
+     */
+    @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+    public void setWidgetPreview(@NonNull ComponentName provider,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+            @NonNull RemoteViews preview) {
+        try {
+            mService.setWidgetPreview(provider, widgetCategories, preview);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the RemoteViews previews for this widget.
+     *
+     * @param provider The {@link ComponentName} for the {@link android.content.BroadcastReceiver
+     *    BroadcastReceiver} provider for the AppWidget you intend to get a preview for.
+     * @param profile The profile in which the provider resides. Passing null is equivalent
+     *        to querying for only the calling user.
+     * @param widgetCategory The widget category for which you want to display previews. This should
+     *    be a single category. If a combination of categories is provided, this function will
+     *    return a preview that matches at least one of the categories.
+     *
+     * @return The widget preview for the selected category, if available.
+     * @see AppWidgetProviderInfo#generatedPreviewCategories
+     */
+    @Nullable
+    @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+    public RemoteViews getWidgetPreview(@NonNull ComponentName provider,
+            @Nullable UserHandle profile, @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+        try {
+            if (profile == null) {
+                profile = mContext.getUser();
+            }
+            return mService.getWidgetPreview(mPackageName, provider, profile.getIdentifier(),
+                    widgetCategory);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove this provider's preview for the specified widget categories. If the provider does not
+     * have a preview for the specified widget category, this is a no-op.
+     *
+     * @param provider The AppWidgetProvider to remove previews for.
+     * @param widgetCategories The categories of the preview to remove. For example, removing the
+     *    preview for WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD will remove the
+     *    previews for both categories.
+     */
+    @FlaggedApi(Flags.FLAG_GENERATED_PREVIEWS)
+    public void removeWidgetPreview(@NonNull ComponentName provider,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+        try {
+            mService.removeWidgetPreview(provider, widgetCategories);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
     @UiThread
     private static @NonNull Executor createUpdateExecutorIfNull() {
         if (sUpdateExecutor == null) {
diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java
index e56e53a..1a80cac2 100644
--- a/core/java/android/appwidget/AppWidgetProviderInfo.java
+++ b/core/java/android/appwidget/AppWidgetProviderInfo.java
@@ -16,6 +16,10 @@
 
 package android.appwidget;
 
+import static android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS;
+import static android.appwidget.flags.Flags.generatedPreviews;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IdRes;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -358,6 +362,20 @@
     /** @hide */
     public boolean isExtendedFromAppWidgetProvider;
 
+    /**
+     * Flags indicating the widget categories for which generated previews are available.
+     * These correspond to the previews set by this provider with
+     * {@link AppWidgetManager#setWidgetPreview}.
+     *
+     * @see #WIDGET_CATEGORY_HOME_SCREEN
+     * @see #WIDGET_CATEGORY_KEYGUARD
+     * @see #WIDGET_CATEGORY_SEARCHBOX
+     * @see AppWidgetManager#getWidgetPreview
+     */
+    @FlaggedApi(FLAG_GENERATED_PREVIEWS)
+    @SuppressLint("MutableBareField")
+    public int generatedPreviewCategories;
+
     public AppWidgetProviderInfo() {
 
     }
@@ -391,6 +409,9 @@
         this.widgetFeatures = in.readInt();
         this.descriptionRes = in.readInt();
         this.isExtendedFromAppWidgetProvider = in.readBoolean();
+        if (generatedPreviews()) {
+            generatedPreviewCategories = in.readInt();
+        }
     }
 
     /**
@@ -515,6 +536,9 @@
         out.writeInt(this.widgetFeatures);
         out.writeInt(this.descriptionRes);
         out.writeBoolean(this.isExtendedFromAppWidgetProvider);
+        if (generatedPreviews()) {
+            out.writeInt(this.generatedPreviewCategories);
+        }
     }
 
     @Override
@@ -545,6 +569,9 @@
         that.widgetFeatures = this.widgetFeatures;
         that.descriptionRes = this.descriptionRes;
         that.isExtendedFromAppWidgetProvider = this.isExtendedFromAppWidgetProvider;
+        if (generatedPreviews()) {
+            that.generatedPreviewCategories = this.generatedPreviewCategories;
+        }
         return that;
     }
 
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 12229b1..6eab363 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -35,6 +35,9 @@
 import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusConfig;
+import android.hardware.input.VirtualStylusMotionEvent;
 import android.hardware.input.VirtualTouchEvent;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.hardware.input.VirtualNavigationTouchpadConfig;
@@ -144,6 +147,12 @@
     void createVirtualNavigationTouchpad(in VirtualNavigationTouchpadConfig config, IBinder token);
 
     /**
+     * Creates a new stylus and registers it with the input framework with the given token.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    void createVirtualStylus(in VirtualStylusConfig config, IBinder token);
+
+    /**
      * Removes the input device corresponding to the given token from the framework.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
@@ -156,32 +165,32 @@
     int getInputDeviceId(IBinder token);
 
     /**
-    * Injects a key event to the virtual dpad corresponding to the given token.
-    */
+     * Injects a key event to the virtual dpad corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
 
     /**
-    * Injects a key event to the virtual keyboard corresponding to the given token.
-    */
+     * Injects a key event to the virtual keyboard corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
 
     /**
-    * Injects a button event to the virtual mouse corresponding to the given token.
-    */
+     * Injects a button event to the virtual mouse corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
 
     /**
-    * Injects a relative event to the virtual mouse corresponding to the given token.
-    */
+     * Injects a relative event to the virtual mouse corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
 
     /**
-    * Injects a scroll event to the virtual mouse corresponding to the given token.
-    */
+     * Injects a scroll event to the virtual mouse corresponding to the given token.
+     */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
 
@@ -192,6 +201,18 @@
     boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
 
     /**
+     * Injects a motion event from the virtual stylus input device corresponding to the given token.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    boolean sendStylusMotionEvent(IBinder token, in VirtualStylusMotionEvent event);
+
+    /**
+     * Injects a button event from the virtual stylus input device corresponding to the given token.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    boolean sendStylusButtonEvent(IBinder token, in VirtualStylusButtonEvent event);
+
+    /**
      * Returns all virtual sensors created for this device.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 2abeeee..c1e443d 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -42,6 +42,8 @@
 import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualNavigationTouchpad;
 import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylus;
+import android.hardware.input.VirtualStylusConfig;
 import android.hardware.input.VirtualTouchscreen;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.media.AudioManager;
@@ -316,6 +318,19 @@
         }
     }
 
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    @NonNull
+    VirtualStylus createVirtualStylus(@NonNull VirtualStylusConfig config) {
+        try {
+            final IBinder token = new Binder(
+                    "android.hardware.input.VirtualStylus:" + config.getInputDeviceName());
+            mVirtualDevice.createVirtualStylus(config, token);
+            return new VirtualStylus(config, mVirtualDevice, token);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     @NonNull
     VirtualNavigationTouchpad createVirtualNavigationTouchpad(
             @NonNull VirtualNavigationTouchpadConfig config) {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index eef60f1..a4cada2 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -55,6 +55,8 @@
 import android.hardware.input.VirtualMouseConfig;
 import android.hardware.input.VirtualNavigationTouchpad;
 import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylus;
+import android.hardware.input.VirtualStylusConfig;
 import android.hardware.input.VirtualTouchscreen;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.media.AudioManager;
@@ -859,6 +861,19 @@
         }
 
         /**
+         * Creates a virtual stylus.
+         *
+         * @param config the touchscreen configurations for the virtual stylus.
+         */
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+        public VirtualStylus createVirtualStylus(
+                @NonNull VirtualStylusConfig config) {
+            return mVirtualDeviceInternal.createVirtualStylus(config);
+        }
+
+        /**
          * Creates a VirtualAudioDevice, capable of recording audio emanating from this device,
          * or injecting audio from another device.
          *
@@ -886,11 +901,14 @@
         }
 
         /**
-         * Creates a new virtual camera. If a virtual camera was already created, it will be closed.
+         * Creates a new virtual camera with the given {@link VirtualCameraConfig}. A virtual device
+         * can create a virtual camera only if it has
+         * {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM} as its
+         * {@link VirtualDeviceParams#POLICY_TYPE_CAMERA}.
          *
-         * @param config camera config.
-         * @return newly created camera;
-         * @hide
+         * @param config camera configuration.
+         * @return newly created camera.
+         * @see VirtualDeviceParams#POLICY_TYPE_CAMERA
          */
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 0253ddd..f9a9da1 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -159,7 +159,7 @@
      * @hide
      */
     @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
-            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY})
+            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
     public @interface PolicyType {}
@@ -246,6 +246,23 @@
     @FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD)
     public static final int POLICY_TYPE_CLIPBOARD = 4;
 
+    /**
+     * Tells the camera framework how to handle camera requests for the front and back cameras from
+     * contexts associated with this virtual device.
+     *
+     * <ul>
+     *     <li>{@link #DEVICE_POLICY_DEFAULT}: Returns the front and back cameras of the default
+     *     device.
+     *     <li>{@link #DEVICE_POLICY_CUSTOM}: Returns the front and back cameras cameras of the
+     *     virtual device. Note that if the virtual device did not create any virtual cameras,
+     *     then no front and back cameras will be available.
+     * </ul>
+     *
+     * @see Context#getDeviceId
+     */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+    public static final int POLICY_TYPE_CAMERA = 5;
+
     private final int mLockState;
     @NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
     @NavigationPolicy
@@ -1153,6 +1170,10 @@
                 mDevicePolicies.delete(POLICY_TYPE_CLIPBOARD);
             }
 
+            if (!Flags.virtualCamera()) {
+                mDevicePolicies.delete(POLICY_TYPE_CAMERA);
+            }
+
             if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE
                     || mAudioRecordingSessionId != AUDIO_SESSION_ID_GENERATE)
                     && mDevicePolicies.get(POLICY_TYPE_AUDIO, DEVICE_POLICY_DEFAULT)
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
index 44942d6..f4f69b5 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
+++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.companion.virtual.camera;
 
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
 import android.view.Surface;
 
 /**
@@ -30,38 +30,36 @@
      * Called when one of the requested stream has been configured by the virtual camera service and
      * is ready to receive data onto its {@link Surface}
      *
-     * @param streamId     The id of the configured stream
-     * @param surface      The surface to write data into for this stream
-     * @param streamConfig The image data configuration for this stream
+     * @param streamId The id of the configured stream
+     * @param surface The surface to write data into for this stream
+     * @param width The width of the surface
+     * @param height The height of the surface
+     * @param format The pixel format of the surface
      */
-    oneway void onStreamConfigured(
-            int streamId,
-            in Surface surface,
-            in VirtualCameraStreamConfig streamConfig);
+    oneway void onStreamConfigured(int streamId, in Surface surface, int width, int height,
+            int format);
 
     /**
      * The client application is requesting a camera frame for the given streamId and frameId.
      *
      * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
-     * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
-     * VirtualCameraStreamConfig)} call.
+     * this stream that was provided during the
+     * {@link #onStreamConfigured(int, Surface, int, int, int)} call.
      *
      * @param streamId The streamId for which the frame is requested. This corresponds to the
-     *     streamId that was given in {@link #onStreamConfigured(int, Surface,
-     *     VirtualCameraStreamConfig)}
+     *     streamId that was given in {@link #onStreamConfigured(int, Surface, int, int, int)}
      * @param frameId The frameId that is being requested. Each request will have a different
      *     frameId, that will be increasing for each call with a particular streamId.
      */
     oneway void onProcessCaptureRequest(int streamId, long frameId);
 
     /**
-     * The stream previously configured when {@link #onStreamConfigured(int, Surface,
-     * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
-     * freed. The Surface was disposed on the client side and should not be used anymore by the
-     * virtual camera owner.
+     * The stream previously configured when
+     * {@link #onStreamConfigured(int, Surface, int, int, int)} was called is now being closed and
+     * associated resources can be freed. The Surface was disposed on the client side and should not
+     * be used anymore by the virtual camera owner.
      *
      * @param streamId The id of the stream that was closed.
      */
     oneway void onStreamClosed(int streamId);
-
 }
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index 5b658b8..c894de4 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -17,9 +17,11 @@
 package android.companion.virtual.camera;
 
 import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.companion.virtual.flags.Flags;
+import android.graphics.ImageFormat;
 import android.view.Surface;
 
 import java.util.concurrent.Executor;
@@ -41,33 +43,33 @@
      *
      * @param streamId The id of the configured stream
      * @param surface The surface to write data into for this stream
-     * @param streamConfig The image data configuration for this stream
+     * @param width The width of the surface
+     * @param height The height of the surface
+     * @param format The {@link ImageFormat} of the surface
      */
-    void onStreamConfigured(
-            int streamId,
-            @NonNull Surface surface,
-            @NonNull VirtualCameraStreamConfig streamConfig);
+    void onStreamConfigured(int streamId, @NonNull Surface surface,
+            @IntRange(from = 1) int width, @IntRange(from = 1) int height,
+            @ImageFormat.Format int format);
 
     /**
      * The client application is requesting a camera frame for the given streamId and frameId.
      *
      * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
-     * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
-     * VirtualCameraStreamConfig)} call.
+     * this stream that was provided during the
+     * {@link #onStreamConfigured(int, Surface, int, int, int)} call.
      *
      * @param streamId The streamId for which the frame is requested. This corresponds to the
-     *     streamId that was given in {@link #onStreamConfigured(int, Surface,
-     *     VirtualCameraStreamConfig)}
+     *     streamId that was given in {@link #onStreamConfigured(int, Surface, int, int, int)}
      * @param frameId The frameId that is being requested. Each request will have a different
      *     frameId, that will be increasing for each call with a particular streamId.
      */
     default void onProcessCaptureRequest(int streamId, long frameId) {}
 
     /**
-     * The stream previously configured when {@link #onStreamConfigured(int, Surface,
-     * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
-     * freed. The Surface corresponding to that streamId was disposed on the client side and should
-     * not be used anymore by the virtual camera owner
+     * The stream previously configured when
+     * {@link #onStreamConfigured(int, Surface, int, int, int)} was called is now being closed and
+     * associated resources can be freed. The Surface corresponding to that streamId was disposed on
+     * the client side and should not be used anymore by the virtual camera owner.
      *
      * @param streamId The id of the stream that was closed.
      */
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
index 88c27a5..5ceb4d1 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.companion.virtual.camera;
 
 /** @hide */
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index 59fe9a1..350cf3d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -19,16 +19,23 @@
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.companion.virtual.VirtualDevice;
 import android.companion.virtual.flags.Flags;
 import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.hardware.camera2.CameraMetadata;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArraySet;
 import android.view.Surface;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
@@ -43,16 +50,57 @@
 @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
 public final class VirtualCameraConfig implements Parcelable {
 
+    private static final int LENS_FACING_UNKNOWN = -1;
+
+    /**
+     * Sensor orientation of {@code 0} degrees.
+     * @see #getSensorOrientation
+     */
+    public static final int SENSOR_ORIENTATION_0 = 0;
+    /**
+     * Sensor orientation of {@code 90} degrees.
+     * @see #getSensorOrientation
+     */
+    public static final int SENSOR_ORIENTATION_90 = 90;
+    /**
+     * Sensor orientation of {@code 180} degrees.
+     * @see #getSensorOrientation
+     */
+    public static final int SENSOR_ORIENTATION_180 = 180;
+    /**
+     * Sensor orientation of {@code 270} degrees.
+     * @see #getSensorOrientation
+     */
+    public static final int SENSOR_ORIENTATION_270 = 270;
+    /** @hide */
+    @IntDef(prefix = {"SENSOR_ORIENTATION_"}, value = {
+            SENSOR_ORIENTATION_0,
+            SENSOR_ORIENTATION_90,
+            SENSOR_ORIENTATION_180,
+            SENSOR_ORIENTATION_270
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SensorOrientation {}
+
     private final String mName;
     private final Set<VirtualCameraStreamConfig> mStreamConfigurations;
     private final IVirtualCameraCallback mCallback;
+    @SensorOrientation
+    private final int mSensorOrientation;
+    private final int mLensFacing;
 
     private VirtualCameraConfig(
             @NonNull String name,
             @NonNull Set<VirtualCameraStreamConfig> streamConfigurations,
             @NonNull Executor executor,
-            @NonNull VirtualCameraCallback callback) {
+            @NonNull VirtualCameraCallback callback,
+            @SensorOrientation int sensorOrientation,
+            int lensFacing) {
         mName = requireNonNull(name, "Missing name");
+        if (lensFacing == LENS_FACING_UNKNOWN) {
+            throw new IllegalArgumentException("Lens facing must be set");
+        }
+        mLensFacing = lensFacing;
         mStreamConfigurations =
                 Set.copyOf(requireNonNull(streamConfigurations, "Missing stream configurations"));
         if (mStreamConfigurations.isEmpty()) {
@@ -63,6 +111,7 @@
                 new VirtualCameraCallbackInternal(
                         requireNonNull(callback, "Missing callback"),
                         requireNonNull(executor, "Missing callback executor"));
+        mSensorOrientation = sensorOrientation;
     }
 
     private VirtualCameraConfig(@NonNull Parcel in) {
@@ -73,6 +122,8 @@
                         in.readParcelableArray(
                                 VirtualCameraStreamConfig.class.getClassLoader(),
                                 VirtualCameraStreamConfig.class));
+        mSensorOrientation = in.readInt();
+        mLensFacing = in.readInt();
     }
 
     @Override
@@ -86,6 +137,8 @@
         dest.writeStrongInterface(mCallback);
         dest.writeParcelableArray(
                 mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]), flags);
+        dest.writeInt(mSensorOrientation);
+        dest.writeInt(mLensFacing);
     }
 
     /**
@@ -100,7 +153,7 @@
      * Returns an unmodifiable set of the stream configurations added to this {@link
      * VirtualCameraConfig}.
      *
-     * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int)
+     * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int, int)
      */
     @NonNull
     public Set<VirtualCameraStreamConfig> getStreamConfigs() {
@@ -118,13 +171,33 @@
     }
 
     /**
+     * Returns the sensor orientation of this stream, which represents the clockwise angle (in
+     * degrees) through which the output image needs to be rotated to be upright on the device
+     * screen in its native orientation. Returns {@link #SENSOR_ORIENTATION_0} if omitted.
+     */
+    @SensorOrientation
+    public int getSensorOrientation() {
+        return mSensorOrientation;
+    }
+
+    /**
+     * Returns the direction that the virtual camera faces relative to the virtual device's screen.
+     *
+     * @see Builder#setLensFacing(int)
+     */
+    public int getLensFacing() {
+        return mLensFacing;
+    }
+
+    /**
      * Builder for {@link VirtualCameraConfig}.
      *
      * <p>To build an instance of {@link VirtualCameraConfig} the following conditions must be met:
-     * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int)}.
+     * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int, int)}.
      * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor,
      *     VirtualCameraCallback)}
      * <li>A camera name must be set with {@link #setName(String)}
+     * <li>A lens facing must be set with {@link #setLensFacing(int)}
      */
     @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
     public static final class Builder {
@@ -133,9 +206,11 @@
         private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>();
         private Executor mCallbackExecutor;
         private VirtualCameraCallback mCallback;
+        private int mSensorOrientation = SENSOR_ORIENTATION_0;
+        private int mLensFacing = LENS_FACING_UNKNOWN;
 
         /**
-         * Set the name of the virtual camera instance.
+         * Sets the name of the virtual camera instance.
          */
         @NonNull
         public Builder setName(@NonNull String name) {
@@ -144,25 +219,94 @@
         }
 
         /**
-         * Add an available stream configuration fot this {@link VirtualCamera}.
+         * Adds a supported input stream configuration for this {@link VirtualCamera}.
          *
          * <p>At least one {@link VirtualCameraStreamConfig} must be added.
          *
          * @param width The width of the stream.
          * @param height The height of the stream.
-         * @param format The {@link ImageFormat} of the stream.
+         * @param format The input format of the stream. Supported formats are
+         *               {@link ImageFormat#YUV_420_888} and {@link PixelFormat#RGBA_8888}.
+         * @param maximumFramesPerSecond The maximum frame rate (in frames per second) for the
+         *                               stream.
          *
-         * @throws IllegalArgumentException if invalid format or dimensions are passed.
+         * @throws IllegalArgumentException if invalid dimensions, format or frame rate are passed.
          */
         @NonNull
-        public Builder addStreamConfig(int width, int height, @ImageFormat.Format int format) {
-            if (width <= 0 || height <= 0) {
-                throw new IllegalArgumentException("Invalid dimensions passed for stream config");
+        public Builder addStreamConfig(
+                @IntRange(from = 1) int width,
+                @IntRange(from = 1) int height,
+                @ImageFormat.Format int format,
+                @IntRange(from = 1) int maximumFramesPerSecond) {
+            // TODO(b/310857519): Check dimension upper limits based on the maximum texture size
+            // supported by the current device, instead of hardcoded limits.
+            if (width <= 0 || width > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+                throw new IllegalArgumentException(
+                        "Invalid width passed for stream config: " + width
+                                + ", must be between 1 and "
+                                + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
             }
-            if (!ImageFormat.isPublicFormat(format)) {
-                throw new IllegalArgumentException("Invalid format passed for stream config");
+            if (height <= 0 || height > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+                throw new IllegalArgumentException(
+                        "Invalid height passed for stream config: " + height
+                                + ", must be between 1 and "
+                                + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
             }
-            mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format));
+            if (!isFormatSupported(format)) {
+                throw new IllegalArgumentException(
+                        "Invalid format passed for stream config: " + format);
+            }
+            if (maximumFramesPerSecond <= 0
+                    || maximumFramesPerSecond > VirtualCameraStreamConfig.MAX_FPS_UPPER_LIMIT) {
+                throw new IllegalArgumentException(
+                        "Invalid maximumFramesPerSecond, must be greater than 0 and less than "
+                                + VirtualCameraStreamConfig.MAX_FPS_UPPER_LIMIT);
+            }
+            mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format,
+                    maximumFramesPerSecond));
+            return this;
+        }
+
+        /**
+         * Sets the sensor orientation of the virtual camera. This field is optional and can be
+         * omitted (defaults to {@link #SENSOR_ORIENTATION_0}).
+         *
+         * @param sensorOrientation The sensor orientation of the camera, which represents the
+         *                          clockwise angle (in degrees) through which the output image
+         *                          needs to be rotated to be upright on the device screen in its
+         *                          native orientation.
+         */
+        @NonNull
+        public Builder setSensorOrientation(@SensorOrientation int sensorOrientation) {
+            if (sensorOrientation != SENSOR_ORIENTATION_0
+                    && sensorOrientation != SENSOR_ORIENTATION_90
+                    && sensorOrientation != SENSOR_ORIENTATION_180
+                    && sensorOrientation != SENSOR_ORIENTATION_270) {
+                throw new IllegalArgumentException(
+                        "Invalid sensor orientation: " + sensorOrientation);
+            }
+            mSensorOrientation = sensorOrientation;
+            return this;
+        }
+
+        /**
+         * Sets the lens facing direction of the virtual camera, can be either
+         * {@link CameraMetadata#LENS_FACING_FRONT} or {@link CameraMetadata#LENS_FACING_BACK}.
+         *
+         * <p>A {@link VirtualDevice} can have at most one {@link VirtualCamera} with
+         * {@link CameraMetadata#LENS_FACING_FRONT} and at most one {@link VirtualCamera} with
+         * {@link CameraMetadata#LENS_FACING_BACK}.
+         *
+         * @param lensFacing The direction that the virtual camera faces relative to the device's
+         *                   screen.
+         */
+        @NonNull
+        public Builder setLensFacing(int lensFacing) {
+            if (lensFacing != CameraMetadata.LENS_FACING_BACK
+                    && lensFacing != CameraMetadata.LENS_FACING_FRONT) {
+                throw new IllegalArgumentException("Unsupported lens facing: " + lensFacing);
+            }
+            mLensFacing = lensFacing;
             return this;
         }
 
@@ -189,11 +333,13 @@
          * Builds a new instance of {@link VirtualCameraConfig}
          *
          * @throws NullPointerException if some required parameters are missing.
+         * @throws IllegalArgumentException if any parameter is invalid.
          */
         @NonNull
         public VirtualCameraConfig build() {
             return new VirtualCameraConfig(
-                    mName, mStreamConfigurations, mCallbackExecutor, mCallback);
+                    mName, mStreamConfigurations, mCallbackExecutor, mCallback, mSensorOrientation,
+                    mLensFacing);
         }
     }
 
@@ -208,9 +354,10 @@
         }
 
         @Override
-        public void onStreamConfigured(
-                int streamId, Surface surface, VirtualCameraStreamConfig streamConfig) {
-            mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, streamConfig));
+        public void onStreamConfigured(int streamId, Surface surface, int width, int height,
+                int format) {
+            mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, width, height,
+                    format));
         }
 
         @Override
@@ -237,4 +384,11 @@
                     return new VirtualCameraConfig[size];
                 }
             };
+
+    private static boolean isFormatSupported(@ImageFormat.Format int format) {
+        return switch (format) {
+            case ImageFormat.YUV_420_888, PixelFormat.RGBA_8888 -> true;
+            default -> false;
+        };
+    }
 }
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index 1272f16..00a814e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.companion.virtual.camera;
 
 import android.annotation.FlaggedApi;
@@ -24,6 +25,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.Objects;
 
 /**
@@ -34,32 +37,47 @@
 @SystemApi
 @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
 public final class VirtualCameraStreamConfig implements Parcelable {
+    // TODO(b/310857519): Check if we should increase the fps upper limit in future.
+    static final int MAX_FPS_UPPER_LIMIT = 60;
+    // This is the minimum guaranteed upper bound of texture size supported by all devices.
+    // Keep this in sync with kMaxTextureSize from services/camera/virtualcamera/util/Util.cc
+    // TODO(b/310857519): Remove this once we add support for fetching the maximum texture size
+    // supported by the current device.
+    static final int DIMENSION_UPPER_LIMIT = 2048;
 
     private final int mWidth;
     private final int mHeight;
     private final int mFormat;
+    private final int mMaxFps;
 
     /**
      * Construct a new instance of {@link VirtualCameraStreamConfig} initialized with the provided
-     * width, height and {@link ImageFormat}
+     * width, height and {@link ImageFormat}.
      *
      * @param width The width of the stream.
      * @param height The height of the stream.
      * @param format The {@link ImageFormat} of the stream.
+     * @param maxFps The maximum frame rate (in frames per second) for the stream.
+     *
+     * @hide
      */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public VirtualCameraStreamConfig(
             @IntRange(from = 1) int width,
             @IntRange(from = 1) int height,
-            @ImageFormat.Format int format) {
+            @ImageFormat.Format int format,
+            @IntRange(from = 1) int maxFps) {
         this.mWidth = width;
         this.mHeight = height;
         this.mFormat = format;
+        this.mMaxFps = maxFps;
     }
 
     private VirtualCameraStreamConfig(@NonNull Parcel in) {
         mWidth = in.readInt();
         mHeight = in.readInt();
         mFormat = in.readInt();
+        mMaxFps = in.readInt();
     }
 
     @Override
@@ -72,6 +90,7 @@
         dest.writeInt(mWidth);
         dest.writeInt(mHeight);
         dest.writeInt(mFormat);
+        dest.writeInt(mMaxFps);
     }
 
     @NonNull
@@ -105,12 +124,13 @@
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         VirtualCameraStreamConfig that = (VirtualCameraStreamConfig) o;
-        return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat;
+        return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat
+                && mMaxFps == that.mMaxFps;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mWidth, mHeight, mFormat);
+        return Objects.hash(mWidth, mHeight, mFormat, mMaxFps);
     }
 
     /** Returns the {@link ImageFormat} of this stream. */
@@ -118,4 +138,10 @@
     public int getFormat() {
         return mFormat;
     }
+
+    /** Returns the maximum frame rate (in frames per second) of this stream. */
+    @IntRange(from = 1)
+    public int getMaximumFramesPerSecond() {
+        return mMaxFps;
+    }
 }
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index ce2490b..588e4fc 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -100,3 +100,10 @@
   description: "Enable interactive screen mirroring using Virtual Devices"
   bug: "292212199"
 }
+
+flag {
+  name: "virtual_stylus"
+  namespace: "virtual_devices"
+  description: "Enable virtual stylus input"
+  bug: "304829446"
+}
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index a1357c9..bde562d 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -231,7 +231,7 @@
     }
 
     /** @hide */
-    public AttributionSource withToken(@NonNull Binder token) {
+    public AttributionSource withToken(@NonNull IBinder token) {
         return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
                 token, mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext());
     }
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index e9b94c9..c7a75ed 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -41,7 +41,6 @@
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.SQLException;
-import android.multiuser.Flags;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -147,7 +146,6 @@
     private boolean mExported;
     private boolean mNoPerms;
     private boolean mSingleUser;
-    private boolean mSystemUserOnly;
     private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray();
 
     private ThreadLocal<AttributionSource> mCallingAttributionSource;
@@ -379,9 +377,7 @@
                             != PermissionChecker.PERMISSION_GRANTED
                             && getContext().checkUriPermission(userUri, Binder.getCallingPid(),
                             callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                            != PackageManager.PERMISSION_GRANTED
-                            && !deniedAccessSystemUserOnlyProvider(callingUserId,
-                            mSystemUserOnly)) {
+                            != PackageManager.PERMISSION_GRANTED) {
                         FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION,
                                 enumCheckUriPermission,
                                 callingUid, uri.getAuthority(), type);
@@ -869,10 +865,6 @@
     boolean checkUser(int pid, int uid, Context context) {
         final int callingUserId = UserHandle.getUserId(uid);
 
-        if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
-            return false;
-        }
-
         if (callingUserId == context.getUserId() || mSingleUser) {
             return true;
         }
@@ -995,9 +987,6 @@
 
         // last chance, check against any uri grants
         final int callingUserId = UserHandle.getUserId(uid);
-        if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
-            return PermissionChecker.PERMISSION_HARD_DENIED;
-        }
         final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid))
                 ? maybeAddUserId(uri, callingUserId) : uri;
         if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
@@ -2634,7 +2623,6 @@
                 setPathPermissions(info.pathPermissions);
                 mExported = info.exported;
                 mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;
-                mSystemUserOnly = (info.flags & ProviderInfo.FLAG_SYSTEM_USER_ONLY) != 0;
                 setAuthorities(info.authority);
             }
             if (Build.IS_DEBUGGABLE) {
@@ -2768,11 +2756,6 @@
         String auth = uri.getAuthority();
         if (!mSingleUser) {
             int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT);
-            if (deniedAccessSystemUserOnlyProvider(mContext.getUserId(),
-                    mSystemUserOnly)) {
-                throw new SecurityException("Trying to query a SYSTEM user only content"
-                        + " provider from user:" + mContext.getUserId());
-            }
             if (userId != UserHandle.USER_CURRENT
                     && userId != mContext.getUserId()
                     // Since userId specified in content uri, the provider userId would be
@@ -2946,16 +2929,4 @@
             Trace.traceBegin(traceTag, methodName + subInfo);
         }
     }
-    /**
-     * Return true if access to content provider is denied because it's a SYSTEM user only
-     * provider and the calling user is not the SYSTEM user.
-     *
-     * @param callingUserId UserId of the caller accessing the content provider.
-     * @param systemUserOnly true when the content provider is only available for the SYSTEM user.
-     */
-    private static boolean deniedAccessSystemUserOnlyProvider(int callingUserId,
-            boolean systemUserOnly) {
-        return Flags.enableSystemUserOnlyForServicesAndProviders()
-                && (callingUserId != UserHandle.USER_SYSTEM && systemUserOnly);
-    }
 }
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index fa76e39..4bc1237 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -16,6 +16,8 @@
 
 package android.content;
 
+import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS;
+
 import android.annotation.AttrRes;
 import android.annotation.CallbackExecutor;
 import android.annotation.CheckResult;
@@ -296,6 +298,7 @@
             BIND_ALLOW_ACTIVITY_STARTS,
             BIND_INCLUDE_CAPABILITIES,
             BIND_SHARED_ISOLATED_PROCESS,
+            BIND_PACKAGE_ISOLATED_PROCESS,
             BIND_EXTERNAL_SERVICE
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -318,6 +321,7 @@
             BIND_ALLOW_ACTIVITY_STARTS,
             BIND_INCLUDE_CAPABILITIES,
             BIND_SHARED_ISOLATED_PROCESS,
+            BIND_PACKAGE_ISOLATED_PROCESS,
             // Intentionally not include BIND_EXTERNAL_SERVICE, because it'd cause sign-extension.
             // This would allow Android Studio to show a warning, if someone tries to use
             // BIND_EXTERNAL_SERVICE BindServiceFlags.
@@ -511,6 +515,26 @@
      */
     public static final int BIND_SHARED_ISOLATED_PROCESS = 0x00002000;
 
+    /**
+     * Flag for {@link #bindIsolatedService}: Bind the service into a shared isolated process,
+     * but only with other isolated services from the same package that declare the same process
+     * name.
+     *
+     * <p>Specifying this flag allows multiple isolated services defined in the same package to be
+     * running in a single shared isolated process. This shared isolated process must be specified
+     * since this flag will not work with the default application process.
+     *
+     * <p>This flag is different from {@link #BIND_SHARED_ISOLATED_PROCESS} since it only
+     * allows binding services from the same package in the same shared isolated process. This also
+     * means the shared package isolated process is global, and not scoped to each potential
+     * calling app.
+     *
+     * <p>The shared isolated process instance is identified by the "android:process" attribute
+     * defined by the service. This flag cannot be used without this attribute set.
+     */
+    @FlaggedApi(FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS)
+    public static final int BIND_PACKAGE_ISOLATED_PROCESS = 1 << 14;
+
     /***********    Public flags above this line ***********/
     /***********    Hidden flags below this line ***********/
 
@@ -6734,7 +6758,7 @@
             @Intent.AccessUriMode int modeFlags);
 
     /**
-     * Determine whether a particular process and user ID has been granted
+     * Determine whether a particular process and uid has been granted
      * permission to access a specific URI.  This only checks for permissions
      * that have been explicitly granted -- if the given process/uid has
      * more general access to the URI's content provider then this check will
@@ -6758,7 +6782,38 @@
             @Intent.AccessUriMode int modeFlags);
 
     /**
-     * Determine whether a particular process and user ID has been granted
+     * Determine whether a particular process and uid has been granted
+     * permission to access a specific content URI.
+     *
+     * <p>Unlike {@link #checkUriPermission(Uri, int, int, int)}, this method
+     * checks for general access to the URI's content provider, as well as
+     * explicitly granted permissions.</p>
+     *
+     * <p>Note, this check will throw an {@link IllegalArgumentException}
+     * for non-content URIs.</p>
+     *
+     * @param uri The content uri that is being checked.
+     * @param pid (Optional) The process ID being checked against. If the
+     * pid is unknown, pass -1.
+     * @param uid The UID being checked against.  A uid of 0 is the root
+     * user, which will pass every permission check.
+     * @param modeFlags The access modes to check.
+     *
+     * @return {@link PackageManager#PERMISSION_GRANTED} if the given
+     * pid/uid is allowed to access that uri, or
+     * {@link PackageManager#PERMISSION_DENIED} if it is not.
+     *
+     * @see #checkUriPermission(Uri, int, int, int)
+     */
+    @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
+    @PackageManager.PermissionResult
+    public int checkContentUriPermissionFull(@NonNull Uri uri, int pid, int uid,
+            @Intent.AccessUriMode int modeFlags) {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
+     * Determine whether a particular process and uid has been granted
      * permission to access a list of URIs.  This only checks for permissions
      * that have been explicitly granted -- if the given process/uid has
      * more general access to the URI's content provider then this check will
@@ -6794,7 +6849,7 @@
             @Intent.AccessUriMode int modeFlags, IBinder callerToken);
 
     /**
-     * Determine whether the calling process and user ID has been
+     * Determine whether the calling process and uid has been
      * granted permission to access a specific URI.  This is basically
      * the same as calling {@link #checkUriPermission(Uri, int, int,
      * int)} with the pid and uid returned by {@link
@@ -6817,7 +6872,7 @@
     public abstract int checkCallingUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags);
 
     /**
-     * Determine whether the calling process and user ID has been
+     * Determine whether the calling process and uid has been
      * granted permission to access a list of URIs.  This is basically
      * the same as calling {@link #checkUriPermissions(List, int, int, int)}
      * with the pid and uid returned by {@link
@@ -6911,7 +6966,7 @@
             @Intent.AccessUriMode int modeFlags);
 
     /**
-     * If a particular process and user ID has not been granted
+     * If a particular process and uid has not been granted
      * permission to access a specific URI, throw {@link
      * SecurityException}.  This only checks for permissions that have
      * been explicitly granted -- if the given process/uid has more
@@ -6931,7 +6986,7 @@
             Uri uri, int pid, int uid, @Intent.AccessUriMode int modeFlags, String message);
 
     /**
-     * If the calling process and user ID has not been granted
+     * If the calling process and uid has not been granted
      * permission to access a specific URI, throw {@link
      * SecurityException}.  This is basically the same as calling
      * {@link #enforceUriPermission(Uri, int, int, int, String)} with
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 0a8029c..e0cf0a5 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -17,6 +17,7 @@
 package android.content;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -1003,6 +1004,12 @@
         return mBase.checkUriPermission(uri, pid, uid, modeFlags);
     }
 
+    @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
+    @Override
+    public int checkContentUriPermissionFull(@NonNull Uri uri, int pid, int uid, int modeFlags) {
+        return mBase.checkContentUriPermissionFull(uri, pid, uid, modeFlags);
+    }
+
     @NonNull
     @Override
     public int[] checkUriPermissions(@NonNull List<Uri> uris, int pid, int uid,
diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig
new file mode 100644
index 0000000..3445fb5
--- /dev/null
+++ b/core/java/android/content/flags/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.content.flags"
+
+flag {
+    name: "enable_bind_package_isolated_process"
+    namespace: "machine_learning"
+    description: "This flag enables the newly added flag for binding package-private isolated processes."
+    bug: "312706530"
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java
index de33fa8..9e553db 100644
--- a/core/java/android/content/pm/ProviderInfo.java
+++ b/core/java/android/content/pm/ProviderInfo.java
@@ -89,15 +89,6 @@
     public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
 
     /**
-     * Bit in {@link #flags}: If set, this provider will only be available
-     * for the system user.
-     * Set from the android.R.attr#systemUserOnly attribute.
-     * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
-     * @hide
-     */
-    public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
-
-    /**
      * Bit in {@link #flags}: If set, a single instance of the provider will
      * run for all users on the device.  Set from the
      * {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 2b378b1..ae46c027 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -101,14 +101,6 @@
     public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
 
     /**
-     * @hide Bit in {@link #flags}: If set, this service will only be available
-     * for the system user.
-     * Set from the android.R.attr#systemUserOnly attribute.
-     * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
-     */
-    public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
-
-    /**
      * Bit in {@link #flags}: If set, a single instance of the service will
      * run for all users on the device.  Set from the
      * {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 1036865..c7797c7 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -70,11 +70,4 @@
     namespace: "profile_experiences"
     description: "Add support for Private Space in resolver sheet"
     bug: "307515485"
-}
-flag {
-    name: "enable_system_user_only_for_services_and_providers"
-    namespace: "multiuser"
-    description: "Enable systemUserOnly manifest attribute for services and providers."
-    bug: "302354856"
-    is_fixed_read_only: true
-}
+}
\ No newline at end of file
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index 47ee76e..796a57b 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -33,12 +33,12 @@
 import android.content.IntentSender;
 import android.os.Binder;
 import android.os.CancellationSignal;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
 import android.provider.DeviceConfig;
 import android.util.Log;
-import android.view.autofill.IAutoFillManagerClient;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -138,7 +138,7 @@
             @CallbackExecutor @NonNull Executor executor,
             @NonNull OutcomeReceiver<GetCandidateCredentialsResponse,
                     GetCandidateCredentialsException> callback,
-            @NonNull IAutoFillManagerClient clientCallback
+            @NonNull IBinder clientCallback
     ) {
         requireNonNull(request, "request must not be null");
         requireNonNull(callingPackage, "callingPackage must not be null");
diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl
index 726bc97..d4a2d97 100644
--- a/core/java/android/credentials/ICredentialManager.aidl
+++ b/core/java/android/credentials/ICredentialManager.aidl
@@ -22,7 +22,6 @@
 import android.credentials.ClearCredentialStateRequest;
 import android.credentials.CreateCredentialRequest;
 import android.credentials.GetCandidateCredentialsRequest;
-import android.view.autofill.IAutoFillManagerClient;
 import android.credentials.GetCredentialRequest;
 import android.credentials.RegisterCredentialDescriptionRequest;
 import android.credentials.UnregisterCredentialDescriptionRequest;
@@ -33,6 +32,7 @@
 import android.credentials.IPrepareGetCredentialCallback;
 import android.credentials.ISetEnabledProvidersCallback;
 import android.content.ComponentName;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 
 /**
@@ -48,7 +48,7 @@
 
     @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage);
 
-    @nullable ICancellationSignal getCandidateCredentials(in GetCredentialRequest request, in IGetCandidateCredentialsCallback callback, in IAutoFillManagerClient clientCallback, String callingPackage);
+    @nullable ICancellationSignal getCandidateCredentials(in GetCredentialRequest request, in IGetCandidateCredentialsCallback callback, in IBinder clientCallback, String callingPackage);
 
     @nullable ICancellationSignal clearCredentialState(in ClearCredentialStateRequest request, in IClearCredentialStateCallback callback, String callingPackage);
 
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 8c1ea5f..a0f4d8d 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -22,6 +22,7 @@
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 import static android.hardware.biometrics.Flags.FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT;
 import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT;
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
@@ -208,6 +209,12 @@
 
         /**
          * Optional: Sets a description that will be shown on the prompt.
+         *
+         * <p> Note that the description set by {@link Builder#setDescription(CharSequence)} will be
+         * overridden by {@link Builder#setContentView(PromptContentView)}. The view provided to
+         * {@link Builder#setContentView(PromptContentView)} will be used if both methods are
+         * called.
+         *
          * @param description The description to display.
          * @return This builder.
          */
@@ -218,6 +225,24 @@
         }
 
         /**
+         * Optional: Sets application customized content view that will be shown on the prompt.
+         *
+         * <p> Note that the description set by {@link Builder#setDescription(CharSequence)} will be
+         * overridden by {@link Builder#setContentView(PromptContentView)}. The view provided to
+         * {@link Builder#setContentView(PromptContentView)} will be used if both methods are
+         * called.
+         *
+         * @param view The customized view information.
+         * @return This builder.re
+         */
+        @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+        @NonNull
+        public BiometricPrompt.Builder setContentView(@NonNull PromptContentView view) {
+            mPromptInfo.setContentView(view);
+            return this;
+        }
+
+        /**
          * @param service
          * @return This builder.
          * @hide
@@ -698,6 +723,18 @@
     }
 
     /**
+     * Gets the content view for the prompt, as set by
+     * {@link Builder#setContentView(PromptContentView)}.
+     *
+     * @return The content view for the prompt, or null if the prompt has no content view.
+     */
+    @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+    @Nullable
+    public PromptContentView getContentView() {
+        return mPromptInfo.getContentView();
+    }
+
+    /**
      * Gets the negative button text for the prompt, as set by
      * {@link Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.
      * @return The negative button text for the prompt, or null if no negative button text was set.
diff --git a/core/java/android/hardware/biometrics/IBiometricContextListener.aidl b/core/java/android/hardware/biometrics/IBiometricContextListener.aidl
index 6ac6581..2f58f51 100644
--- a/core/java/android/hardware/biometrics/IBiometricContextListener.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricContextListener.aidl
@@ -38,4 +38,8 @@
     // Called when the display state of the device changes.
     // Where `displayState` is defined in AuthenticateOptions.DisplayState
     void onDisplayStateChanged(int displayState);
+
+    // Called when the HAL ignoring touches state changes.
+    // When true, the HAL ignores touches on the sensor.
+    void onHardwareIgnoreTouchesChanged(boolean shouldIgnore);
 }
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/biometrics/PromptContentListItem.java
similarity index 66%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/biometrics/PromptContentListItem.java
index ce92b6d..fa3783d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/biometrics/PromptContentListItem.java
@@ -13,10 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
 
 /**
- * The configuration of a single virtual camera stream.
- * @hide
+ * A list item shown on {@link PromptVerticalListContentView}.
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public interface PromptContentListItem {
+}
+
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java
new file mode 100644
index 0000000..c31f8a6
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptContentListItemBulletedText implements PromptContentListItemParcelable {
+    private final CharSequence mText;
+
+    /**
+     * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+     *
+     * @param text The text of this list item.
+     */
+    public PromptContentListItemBulletedText(@NonNull CharSequence text) {
+        mText = text;
+    }
+
+    /**
+     * @hide
+     */
+    @NonNull
+    public CharSequence getText() {
+        return mText;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeCharSequence(mText);
+    }
+
+    /**
+     * @see Parcelable.Creator
+     */
+    @NonNull
+    public static final Creator<PromptContentListItemBulletedText> CREATOR = new Creator<>() {
+        @Override
+        public PromptContentListItemBulletedText createFromParcel(Parcel in) {
+            return new PromptContentListItemBulletedText(in.readCharSequence());
+        }
+
+        @Override
+        public PromptContentListItemBulletedText[] newArray(int size) {
+            return new PromptContentListItemBulletedText[size];
+        }
+    };
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java b/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java
new file mode 100644
index 0000000..15271f0
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcelable;
+
+/**
+ * A parcelable {@link PromptContentListItem}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+sealed interface PromptContentListItemParcelable extends PromptContentListItem, Parcelable
+        permits PromptContentListItemPlainText, PromptContentListItemBulletedText {
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java
new file mode 100644
index 0000000..d72a758
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A list item with plain text shown on {@link PromptVerticalListContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptContentListItemPlainText implements PromptContentListItemParcelable {
+    private final CharSequence mText;
+
+    /**
+     * A list item with plain text shown on {@link PromptVerticalListContentView}.
+     *
+     * @param text The text of this list item.
+     */
+    public PromptContentListItemPlainText(@NonNull CharSequence text) {
+        mText = text;
+    }
+
+    /**
+     * @hide
+     */
+    @NonNull
+    public CharSequence getText() {
+        return mText;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeCharSequence(mText);
+    }
+
+    /**
+     * @see Parcelable.Creator
+     */
+    @NonNull
+    public static final Creator<PromptContentListItemPlainText> CREATOR = new Creator<>() {
+        @Override
+        public PromptContentListItemPlainText createFromParcel(Parcel in) {
+            return new PromptContentListItemPlainText(in.readCharSequence());
+        }
+
+        @Override
+        public PromptContentListItemPlainText[] newArray(int size) {
+            return new PromptContentListItemPlainText[size];
+        }
+    };
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/biometrics/PromptContentView.java
similarity index 65%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/biometrics/PromptContentView.java
index ce92b6d..ff9313e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/biometrics/PromptContentView.java
@@ -13,10 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
 
 /**
- * The configuration of a single virtual camera stream.
- * @hide
+ * Contains the information of the template of content view for Biometric Prompt.
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public interface PromptContentView {
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java
similarity index 60%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/biometrics/PromptContentViewParcelable.java
index ce92b6d..43b965b 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java
@@ -13,10 +13,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcelable;
 
 /**
- * The configuration of a single virtual camera stream.
- * @hide
+ * A parcelable {@link PromptContentView}.
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+sealed interface PromptContentViewParcelable extends PromptContentView, Parcelable
+        permits PromptVerticalListContentView {
+}
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index 24cfd164..c73ebd4 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -35,6 +35,7 @@
     @Nullable private CharSequence mSubtitle;
     private boolean mUseDefaultSubtitle;
     @Nullable private CharSequence mDescription;
+    @Nullable private PromptContentViewParcelable mContentView;
     @Nullable private CharSequence mDeviceCredentialTitle;
     @Nullable private CharSequence mDeviceCredentialSubtitle;
     @Nullable private CharSequence mDeviceCredentialDescription;
@@ -60,6 +61,8 @@
         mSubtitle = in.readCharSequence();
         mUseDefaultSubtitle = in.readBoolean();
         mDescription = in.readCharSequence();
+        mContentView = in.readParcelable(PromptContentViewParcelable.class.getClassLoader(),
+                PromptContentViewParcelable.class);
         mDeviceCredentialTitle = in.readCharSequence();
         mDeviceCredentialSubtitle = in.readCharSequence();
         mDeviceCredentialDescription = in.readCharSequence();
@@ -100,6 +103,7 @@
         dest.writeCharSequence(mSubtitle);
         dest.writeBoolean(mUseDefaultSubtitle);
         dest.writeCharSequence(mDescription);
+        dest.writeParcelable(mContentView, 0);
         dest.writeCharSequence(mDeviceCredentialTitle);
         dest.writeCharSequence(mDeviceCredentialSubtitle);
         dest.writeCharSequence(mDeviceCredentialDescription);
@@ -176,6 +180,10 @@
         mDescription = description;
     }
 
+    public void setContentView(PromptContentView view) {
+        mContentView = (PromptContentViewParcelable) view;
+    }
+
     public void setDeviceCredentialTitle(CharSequence deviceCredentialTitle) {
         mDeviceCredentialTitle = deviceCredentialTitle;
     }
@@ -257,6 +265,15 @@
         return mDescription;
     }
 
+    /**
+     * Gets the content view for the prompt.
+     *
+     * @return The content view for the prompt, or null if the prompt has no content view.
+     */
+    public PromptContentView getContentView() {
+        return mContentView;
+    }
+
     public CharSequence getDeviceCredentialTitle() {
         return mDeviceCredentialTitle;
     }
diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
new file mode 100644
index 0000000..f3cb189
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.biometrics;
+
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Contains the information of the template of vertical list content view for Biometric Prompt. Note
+ * that there are limits on the item count and the number of characters allowed for each item's
+ * text.
+ * <p>
+ * Here's how you'd set a <code>PromptVerticalListContentView</code> on a Biometric Prompt:
+ * <pre class="prettyprint">
+ * BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(...)
+ *     .setTitle(...)
+ *     .setSubTitle(...)
+ *     .setContentView(new PromptVerticalListContentView.Builder()
+ *         .setDescription("test description")
+ *         .addListItem(new PromptContentListItemPlainText("test item 1"))
+ *         .addListItem(new PromptContentListItemPlainText("test item 2"))
+ *         .addListItem(new PromptContentListItemBulletedText("test item 3"))
+ *         .build())
+ *     .build();
+ * </pre>
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptVerticalListContentView implements PromptContentViewParcelable {
+    private static final int MAX_ITEM_NUMBER = 20;
+    private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
+    private final List<PromptContentListItemParcelable> mContentList;
+    private final CharSequence mDescription;
+
+    private PromptVerticalListContentView(
+            @NonNull List<PromptContentListItemParcelable> contentList,
+            @NonNull CharSequence description) {
+        mContentList = contentList;
+        mDescription = description;
+    }
+
+    private PromptVerticalListContentView(Parcel in) {
+        mContentList = in.readArrayList(
+                PromptContentListItemParcelable.class.getClassLoader(),
+                PromptContentListItemParcelable.class);
+        mDescription = in.readCharSequence();
+    }
+
+    /**
+     * Returns the maximum count of the list items.
+     */
+    public static int getMaxItemCount() {
+        return MAX_ITEM_NUMBER;
+    }
+
+    /**
+     * Returns the maximum number of characters allowed for each item's text.
+     */
+    public static int getMaxEachItemCharacterNumber() {
+        return MAX_EACH_ITEM_CHARACTER_NUMBER;
+    }
+
+    /**
+     * Gets the description for the content view, as set by
+     * {@link PromptVerticalListContentView.Builder#setDescription(CharSequence)}.
+     *
+     * @return The description for the content view, or null if the content view has no description.
+     */
+    @Nullable
+    public CharSequence getDescription() {
+        return mDescription;
+    }
+
+    /**
+     * Gets the list of ListItem on the content view, as set by
+     * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentListItem)}.
+     *
+     * @return The item list on the content view.
+     */
+    @NonNull
+    public List<PromptContentListItem> getListItems() {
+        return new ArrayList<>(mContentList);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
+        dest.writeList(mContentList);
+        dest.writeCharSequence(mDescription);
+    }
+
+    /**
+     * @see Parcelable.Creator
+     */
+    @NonNull
+    public static final Creator<PromptVerticalListContentView> CREATOR = new Creator<>() {
+        @Override
+        public PromptVerticalListContentView createFromParcel(Parcel in) {
+            return new PromptVerticalListContentView(in);
+        }
+
+        @Override
+        public PromptVerticalListContentView[] newArray(int size) {
+            return new PromptVerticalListContentView[size];
+        }
+    };
+
+
+    /**
+     * A builder that collects arguments to be shown on the vertical list view.
+     */
+    public static final class Builder {
+        private final List<PromptContentListItemParcelable> mContentList = new ArrayList<>();
+        private CharSequence mDescription;
+
+        /**
+         * Optional: Sets a description that will be shown on the content view.
+         *
+         * @param description The description to display.
+         * @return This builder.
+         */
+        @NonNull
+        public Builder setDescription(@NonNull CharSequence description) {
+            mDescription = description;
+            return this;
+        }
+
+        /**
+         * Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in
+         * total.
+         *
+         * @param listItem The list item view to display
+         * @return This builder.
+         */
+        @NonNull
+        public Builder addListItem(@NonNull PromptContentListItem listItem) {
+            if (doesListItemExceedsCharLimit(listItem)) {
+                throw new IllegalStateException(
+                        "The character number of list item exceeds "
+                                + MAX_EACH_ITEM_CHARACTER_NUMBER);
+            }
+            mContentList.add((PromptContentListItemParcelable) listItem);
+            return this;
+        }
+
+        private boolean doesListItemExceedsCharLimit(PromptContentListItem listItem) {
+            if (listItem instanceof PromptContentListItemPlainText) {
+                return ((PromptContentListItemPlainText) listItem).getText().length()
+                        > MAX_EACH_ITEM_CHARACTER_NUMBER;
+            } else if (listItem instanceof PromptContentListItemBulletedText) {
+                return ((PromptContentListItemBulletedText) listItem).getText().length()
+                        > MAX_EACH_ITEM_CHARACTER_NUMBER;
+            } else {
+                return false;
+            }
+        }
+
+
+        /**
+         * Creates a {@link PromptVerticalListContentView}.
+         *
+         * @return An instance of {@link PromptVerticalListContentView}.
+         */
+        @NonNull
+        public PromptVerticalListContentView build() {
+            if (mContentList.size() > MAX_ITEM_NUMBER) {
+                throw new IllegalStateException(
+                        "The number of list items exceeds " + MAX_ITEM_NUMBER);
+            }
+            return new PromptVerticalListContentView(mContentList, mDescription);
+        }
+    }
+}
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 375fdb5..3ba8be4 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -21,3 +21,10 @@
   bug: "307601768"
 }
 
+flag {
+  name: "custom_biometric_prompt"
+  namespace: "biometrics_framework"
+  description: "Feature flag for adding a custom content view API to BiometricPrompt.Builder."
+  bug: "302735104"
+}
+
diff --git a/core/java/android/hardware/input/VirtualStylus.java b/core/java/android/hardware/input/VirtualStylus.java
new file mode 100644
index 0000000..c763f740
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylus.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.companion.virtual.IVirtualDevice;
+import android.companion.virtual.flags.Flags;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A virtual stylus which can be used to inject input into the framework that represents a stylus
+ * on a remote device.
+ *
+ * This registers an {@link android.view.InputDevice} that is interpreted like a
+ * physically-connected device and dispatches received events to it.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public class VirtualStylus extends VirtualInputDevice {
+    /** @hide */
+    public VirtualStylus(VirtualStylusConfig config, IVirtualDevice virtualDevice,
+            IBinder token) {
+        super(config, virtualDevice, token);
+    }
+
+    /**
+     * Sends a motion event to the system.
+     *
+     * @param event the event to send
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendMotionEvent(@NonNull VirtualStylusMotionEvent event) {
+        try {
+            if (!mVirtualDevice.sendStylusMotionEvent(mToken, event)) {
+                Log.w(TAG, "Failed to send motion event from virtual stylus "
+                        + mConfig.getInputDeviceName());
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sends a button event to the system.
+     *
+     * @param event the event to send
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendButtonEvent(@NonNull VirtualStylusButtonEvent event) {
+        try {
+            if (!mVirtualDevice.sendStylusButtonEvent(mToken, event)) {
+                Log.w(TAG, "Failed to send button event from virtual stylus "
+                        + mConfig.getInputDeviceName());
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl
similarity index 72%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/input/VirtualStylusButtonEvent.aidl
index ce92b6d..7de32cc 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,10 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package android.hardware.input;
+
+parcelable VirtualStylusButtonEvent;
diff --git a/core/java/android/hardware/input/VirtualStylusButtonEvent.java b/core/java/android/hardware/input/VirtualStylusButtonEvent.java
new file mode 100644
index 0000000..97a4cd0
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a stylus button click interaction originating from a remote device.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusButtonEvent implements Parcelable {
+    /** @hide */
+    public static final int ACTION_UNKNOWN = -1;
+    /** Action indicating the stylus button has been pressed. */
+    public static final int ACTION_BUTTON_PRESS = MotionEvent.ACTION_BUTTON_PRESS;
+    /** Action indicating the stylus button has been released. */
+    public static final int ACTION_BUTTON_RELEASE = MotionEvent.ACTION_BUTTON_RELEASE;
+    /** @hide */
+    @IntDef(prefix = {"ACTION_"}, value = {
+            ACTION_UNKNOWN,
+            ACTION_BUTTON_PRESS,
+            ACTION_BUTTON_RELEASE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    /** @hide */
+    public static final int BUTTON_UNKNOWN = -1;
+    /** Action indicating the stylus button involved in this event is primary. */
+    public static final int BUTTON_PRIMARY = MotionEvent.BUTTON_STYLUS_PRIMARY;
+    /** Action indicating the stylus button involved in this event is secondary. */
+    public static final int BUTTON_SECONDARY = MotionEvent.BUTTON_STYLUS_SECONDARY;
+    /** @hide */
+    @IntDef(prefix = {"BUTTON_"}, value = {
+            BUTTON_UNKNOWN,
+            BUTTON_PRIMARY,
+            BUTTON_SECONDARY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Button {}
+
+    @Action
+    private final int mAction;
+    @Button
+    private final int mButtonCode;
+    private final long mEventTimeNanos;
+
+    private VirtualStylusButtonEvent(@Action int action, @Button int buttonCode,
+            long eventTimeNanos) {
+        mAction = action;
+        mButtonCode = buttonCode;
+        mEventTimeNanos = eventTimeNanos;
+    }
+
+    private VirtualStylusButtonEvent(@NonNull Parcel parcel) {
+        mAction = parcel.readInt();
+        mButtonCode = parcel.readInt();
+        mEventTimeNanos = parcel.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) {
+        parcel.writeInt(mAction);
+        parcel.writeInt(mButtonCode);
+        parcel.writeLong(mEventTimeNanos);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the button code associated with this event.
+     */
+    @Button
+    public int getButtonCode() {
+        return mButtonCode;
+    }
+
+    /**
+     * Returns the action associated with this event.
+     */
+    @Action
+    public int getAction() {
+        return mAction;
+    }
+
+    /**
+     * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but
+     * with nanosecond (instead of millisecond) precision.
+     *
+     * @see InputEvent#getEventTime()
+     */
+    public long getEventTimeNanos() {
+        return mEventTimeNanos;
+    }
+
+    /**
+     * Builder for {@link VirtualStylusButtonEvent}.
+     */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+    public static final class Builder {
+
+        @Action
+        private int mAction = ACTION_UNKNOWN;
+        @Button
+        private int mButtonCode = BUTTON_UNKNOWN;
+        private long mEventTimeNanos = 0L;
+
+        /**
+         * Creates a {@link VirtualStylusButtonEvent} object with the current builder configuration.
+         */
+        @NonNull
+        public VirtualStylusButtonEvent build() {
+            if (mAction == ACTION_UNKNOWN) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus button event with unset action");
+            }
+            if (mButtonCode == BUTTON_UNKNOWN) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus button event with unset button code");
+            }
+            return new VirtualStylusButtonEvent(mAction, mButtonCode, mEventTimeNanos);
+        }
+
+        /**
+         * Sets the button code of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setButtonCode(@Button int buttonCode) {
+            if (buttonCode != BUTTON_PRIMARY && buttonCode != BUTTON_SECONDARY) {
+                throw new IllegalArgumentException(
+                        "Unsupported stylus button code : " + buttonCode);
+            }
+            mButtonCode = buttonCode;
+            return this;
+        }
+
+        /**
+         * Sets the action of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setAction(@Action int action) {
+            if (action != ACTION_BUTTON_PRESS && action != ACTION_BUTTON_RELEASE) {
+                throw new IllegalArgumentException("Unsupported stylus button action : " + action);
+            }
+            mAction = action;
+            return this;
+        }
+
+        /**
+         * Sets the time (in nanoseconds) when this specific event was generated. This may be
+         * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of
+         * millisecond), but can be different depending on the use case.
+         * This field is optional and can be omitted.
+         *
+         * @return this builder, to allow for chaining of calls
+         * @see InputEvent#getEventTime()
+         */
+        @NonNull
+        public Builder setEventTimeNanos(long eventTimeNanos) {
+            if (eventTimeNanos < 0L) {
+                throw new IllegalArgumentException("Event time cannot be negative");
+            }
+            this.mEventTimeNanos = eventTimeNanos;
+            return this;
+        }
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<VirtualStylusButtonEvent> CREATOR =
+            new Parcelable.Creator<>() {
+                public VirtualStylusButtonEvent createFromParcel(Parcel source) {
+                    return new VirtualStylusButtonEvent(source);
+                }
+
+                public VirtualStylusButtonEvent[] newArray(int size) {
+                    return new VirtualStylusButtonEvent[size];
+                }
+            };
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/input/VirtualStylusConfig.aidl
similarity index 72%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/input/VirtualStylusConfig.aidl
index ce92b6d..a13eec2 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/input/VirtualStylusConfig.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,10 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package android.hardware.input;
+
+parcelable VirtualStylusConfig;
diff --git a/core/java/android/hardware/input/VirtualStylusConfig.java b/core/java/android/hardware/input/VirtualStylusConfig.java
new file mode 100644
index 0000000..64cf1f5
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusConfig.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Configurations to create a virtual stylus.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusConfig extends VirtualTouchDeviceConfig implements Parcelable {
+
+    private VirtualStylusConfig(@NonNull Builder builder) {
+        super(builder);
+    }
+
+    private VirtualStylusConfig(@NonNull Parcel in) {
+        super(in);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+    }
+
+    @NonNull
+    public static final Creator<VirtualStylusConfig> CREATOR =
+            new Creator<>() {
+                @Override
+                public VirtualStylusConfig createFromParcel(Parcel in) {
+                    return new VirtualStylusConfig(in);
+                }
+
+                @Override
+                public VirtualStylusConfig[] newArray(int size) {
+                    return new VirtualStylusConfig[size];
+                }
+            };
+
+    /**
+     * Builder for creating a {@link VirtualStylusConfig}.
+     */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+    public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> {
+
+        /**
+         * Creates a new instance for the given dimensions of the screen targeted by the
+         * {@link VirtualStylus}.
+         *
+         * <p>The dimensions are not pixels but in the screen's raw coordinate space. They do
+         * not necessarily have to correspond to the display size or aspect ratio. In this case the
+         * framework will handle the scaling appropriately.
+         *
+         * @param screenWidth The width of the targeted screen.
+         * @param screenHeight The height of the targeted screen.
+         */
+        public Builder(@IntRange(from = 1) int screenWidth,
+                @IntRange(from = 1) int screenHeight) {
+            super(screenWidth, screenHeight);
+        }
+
+        /**
+         * Builds the {@link VirtualStylusConfig} instance.
+         */
+        @NonNull
+        public VirtualStylusConfig build() {
+            return new VirtualStylusConfig(this);
+        }
+    }
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl
similarity index 72%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/input/VirtualStylusMotionEvent.aidl
index ce92b6d..42d14ab 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,10 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package android.hardware.input;
+
+parcelable VirtualStylusMotionEvent;
diff --git a/core/java/android/hardware/input/VirtualStylusMotionEvent.java b/core/java/android/hardware/input/VirtualStylusMotionEvent.java
new file mode 100644
index 0000000..2ab76ae
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a stylus interaction originating from a remote device.
+ *
+ * The tool type, location and action are required; tilts and pressure are optional.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusMotionEvent implements Parcelable {
+    private static final int TILT_MIN = -90;
+    private static final int TILT_MAX = 90;
+    private static final int PRESSURE_MIN = 0;
+    private static final int PRESSURE_MAX = 255;
+
+    /** @hide */
+    public static final int TOOL_TYPE_UNKNOWN = MotionEvent.TOOL_TYPE_UNKNOWN;
+    /** Tool type indicating that a stylus is the origin of the event. */
+    public static final int TOOL_TYPE_STYLUS = MotionEvent.TOOL_TYPE_STYLUS;
+    /** Tool type indicating that an eraser is the origin of the event. */
+    public static final int TOOL_TYPE_ERASER = MotionEvent.TOOL_TYPE_ERASER;
+    /** @hide */
+    @IntDef(prefix = { "TOOL_TYPE_" }, value = {
+            TOOL_TYPE_UNKNOWN,
+            TOOL_TYPE_STYLUS,
+            TOOL_TYPE_ERASER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ToolType {}
+
+    /** @hide */
+    public static final int ACTION_UNKNOWN = -1;
+    /**
+     * Action indicating the stylus has been pressed down to the screen. ACTION_DOWN with pressure
+     * {@code 0} indicates that the stylus is hovering over the screen, and non-zero pressure
+     * indicates that the stylus is touching the screen.
+     */
+    public static final int ACTION_DOWN = MotionEvent.ACTION_DOWN;
+    /** Action indicating the stylus has been lifted from the screen. */
+    public static final int ACTION_UP = MotionEvent.ACTION_UP;
+    /** Action indicating the stylus has been moved along the screen. */
+    public static final int ACTION_MOVE = MotionEvent.ACTION_MOVE;
+    /** @hide */
+    @IntDef(prefix = { "ACTION_" }, value = {
+            ACTION_UNKNOWN,
+            ACTION_DOWN,
+            ACTION_UP,
+            ACTION_MOVE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    @ToolType
+    private final int mToolType;
+    @Action
+    private final int mAction;
+    private final int mX;
+    private final int mY;
+    private final int mPressure;
+    private final int mTiltX;
+    private final int mTiltY;
+    private final long mEventTimeNanos;
+
+    private VirtualStylusMotionEvent(@ToolType int toolType, @Action int action, int x, int y,
+            int pressure, int tiltX, int tiltY, long eventTimeNanos) {
+        mToolType = toolType;
+        mAction = action;
+        mX = x;
+        mY = y;
+        mPressure = pressure;
+        mTiltX = tiltX;
+        mTiltY = tiltY;
+        mEventTimeNanos = eventTimeNanos;
+    }
+
+    private VirtualStylusMotionEvent(@NonNull Parcel parcel) {
+        mToolType = parcel.readInt();
+        mAction = parcel.readInt();
+        mX = parcel.readInt();
+        mY = parcel.readInt();
+        mPressure = parcel.readInt();
+        mTiltX = parcel.readInt();
+        mTiltY = parcel.readInt();
+        mEventTimeNanos = parcel.readLong();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mToolType);
+        dest.writeInt(mAction);
+        dest.writeInt(mX);
+        dest.writeInt(mY);
+        dest.writeInt(mPressure);
+        dest.writeInt(mTiltX);
+        dest.writeInt(mTiltY);
+        dest.writeLong(mEventTimeNanos);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the tool type associated with this event.
+     */
+    @ToolType
+    public int getToolType() {
+        return mToolType;
+    }
+
+    /**
+     * Returns the action associated with this event.
+     */
+    @Action
+    public int getAction() {
+        return mAction;
+    }
+
+    /**
+     * Returns the x-axis location associated with this event.
+     */
+    public int getX() {
+        return mX;
+    }
+
+    /**
+     * Returns the y-axis location associated with this event.
+     */
+    public int getY() {
+        return mY;
+    }
+
+    /**
+     * Returns the pressure associated with this event. {@code 0} pressure indicates that the stylus
+     * is hovering, otherwise the stylus is touching the screen. Returns {@code 255} if omitted.
+     */
+    public int getPressure() {
+        return mPressure;
+    }
+
+    /**
+     * Returns the plane angle (in degrees, in the range of [{@code -90}, {@code 90}]) between the
+     * y-z plane and the plane containing both the stylus axis and the y axis. A positive tiltX is
+     * to the right, in the direction of increasing x values. {@code 0} tilt indicates that the
+     * stylus is perpendicular to the x-axis. Returns {@code 0} if omitted.
+     *
+     * @see Builder#setTiltX
+     */
+    public int getTiltX() {
+        return mTiltX;
+    }
+
+    /**
+     * Returns the plane angle (in degrees, in the range of [{@code -90}, {@code 90}]) between the
+     * x-z plane and the plane containing both the stylus axis and the x axis. A positive tiltY is
+     * towards the user, in the direction of increasing y values. {@code 0} tilt indicates that the
+     * stylus is perpendicular to the y-axis. Returns {@code 0} if omitted.
+     *
+     * @see Builder#setTiltY
+     */
+    public int getTiltY() {
+        return mTiltY;
+    }
+
+    /**
+     * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but
+     * with nanosecond (instead of millisecond) precision.
+     *
+     * @see InputEvent#getEventTime()
+     */
+    public long getEventTimeNanos() {
+        return mEventTimeNanos;
+    }
+
+    /**
+     * Builder for {@link VirtualStylusMotionEvent}.
+     */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+    public static final class Builder {
+
+        @ToolType
+        private int mToolType = TOOL_TYPE_UNKNOWN;
+        @Action
+        private int mAction = ACTION_UNKNOWN;
+        private int mX = 0;
+        private int mY = 0;
+        private boolean mIsXSet = false;
+        private boolean mIsYSet = false;
+        private int mPressure = PRESSURE_MAX;
+        private int mTiltX = 0;
+        private int mTiltY = 0;
+        private long mEventTimeNanos = 0L;
+
+        /**
+         * Creates a {@link VirtualStylusMotionEvent} object with the current builder configuration.
+         *
+         * @throws IllegalArgumentException if one of the required arguments (action, tool type,
+         * x-axis location and y-axis location) is missing.
+         * {@link VirtualStylusMotionEvent} for a detailed explanation.
+         */
+        @NonNull
+        public VirtualStylusMotionEvent build() {
+            if (mToolType == TOOL_TYPE_UNKNOWN) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus motion event with unset tool type");
+            }
+            if (mAction == ACTION_UNKNOWN) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus motion event with unset action");
+            }
+            if (!mIsXSet) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus motion event with unset x-axis location");
+            }
+            if (!mIsYSet) {
+                throw new IllegalArgumentException(
+                        "Cannot build stylus motion event with unset y-axis location");
+            }
+            return new VirtualStylusMotionEvent(mToolType, mAction, mX, mY, mPressure, mTiltX,
+                    mTiltY, mEventTimeNanos);
+        }
+
+        /**
+         * Sets the tool type of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setToolType(@ToolType int toolType) {
+            if (toolType != TOOL_TYPE_STYLUS && toolType != TOOL_TYPE_ERASER) {
+                throw new IllegalArgumentException("Unsupported stylus tool type: " + toolType);
+            }
+            mToolType = toolType;
+            return this;
+        }
+
+        /**
+         * Sets the action of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setAction(@Action int action) {
+            if (action != ACTION_DOWN && action != ACTION_UP && action != ACTION_MOVE) {
+                throw new IllegalArgumentException("Unsupported stylus action : " + action);
+            }
+            mAction = action;
+            return this;
+        }
+
+        /**
+         * Sets the x-axis location of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setX(int absX) {
+            mX = absX;
+            mIsXSet = true;
+            return this;
+        }
+
+        /**
+         * Sets the y-axis location of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setY(int absY) {
+            mY = absY;
+            mIsYSet = true;
+            return this;
+        }
+
+        /**
+         * Sets the pressure of the event. {@code 0} pressure indicates that the stylus is hovering,
+         * otherwise the stylus is touching the screen. This field is optional and can be omitted
+         * (defaults to {@code 255}).
+         *
+         * @param pressure The pressure of the stylus.
+         *
+         * @throws IllegalArgumentException if the pressure is smaller than 0 or greater than 255.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        @NonNull
+        public Builder setPressure(
+                @IntRange(from = PRESSURE_MIN, to = PRESSURE_MAX) int pressure) {
+            if (pressure < PRESSURE_MIN || pressure > PRESSURE_MAX) {
+                throw new IllegalArgumentException(
+                        "Pressure should be between " + PRESSURE_MIN + " and " + PRESSURE_MAX);
+            }
+            mPressure = pressure;
+            return this;
+        }
+
+        /**
+         * Sets the x-axis tilt of the event in degrees. {@code 0} tilt indicates that the stylus is
+         * perpendicular to the x-axis. This field is optional and can be omitted (defaults to
+         * {@code 0}). Both x-axis tilt and y-axis tilt are used to derive the tilt and orientation
+         * of the stylus, given by {@link MotionEvent#AXIS_TILT} and
+         * {@link MotionEvent#AXIS_ORIENTATION} respectively.
+         *
+         * @throws IllegalArgumentException if the tilt is smaller than -90 or greater than 90.
+         *
+         * @return this builder, to allow for chaining of calls
+         *
+         * @see VirtualStylusMotionEvent#getTiltX
+         * @see <a href="https://source.android.com/docs/core/interaction/input/touch-devices#orientation-and-tilt-fields">
+         *     Stylus tilt and orientation</a>
+         */
+        @NonNull
+        public Builder setTiltX(@IntRange(from = TILT_MIN, to = TILT_MAX) int tiltX) {
+            validateTilt(tiltX);
+            mTiltX = tiltX;
+            return this;
+        }
+
+        /**
+         * Sets the y-axis tilt of the event in degrees. {@code 0} tilt indicates that the stylus is
+         * perpendicular to the y-axis. This field is optional and can be omitted (defaults to
+         * {@code 0}). Both x-axis tilt and y-axis tilt are used to derive the tilt and orientation
+         * of the stylus, given by {@link MotionEvent#AXIS_TILT} and
+         * {@link MotionEvent#AXIS_ORIENTATION} respectively.
+         *
+         * @throws IllegalArgumentException if the tilt is smaller than -90 or greater than 90.
+         *
+         * @return this builder, to allow for chaining of calls
+         *
+         * @see VirtualStylusMotionEvent#getTiltY
+         * @see <a href="https://source.android.com/docs/core/interaction/input/touch-devices#orientation-and-tilt-fields">
+         *     Stylus tilt and orientation</a>
+         */
+        @NonNull
+        public Builder setTiltY(@IntRange(from = TILT_MIN, to = TILT_MAX) int tiltY) {
+            validateTilt(tiltY);
+            mTiltY = tiltY;
+            return this;
+        }
+
+        /**
+         * Sets the time (in nanoseconds) when this specific event was generated. This may be
+         * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of
+         * millisecond), but can be different depending on the use case.
+         * This field is optional and can be omitted.
+         *
+         * @return this builder, to allow for chaining of calls
+         * @see InputEvent#getEventTime()
+         */
+        @NonNull
+        public Builder setEventTimeNanos(long eventTimeNanos) {
+            if (eventTimeNanos < 0L) {
+                throw new IllegalArgumentException("Event time cannot be negative");
+            }
+            mEventTimeNanos = eventTimeNanos;
+            return this;
+        }
+
+        private void validateTilt(int tilt) {
+            if (tilt < TILT_MIN || tilt > TILT_MAX) {
+                throw new IllegalArgumentException(
+                        "Tilt must be between " + TILT_MIN + " and " + TILT_MAX);
+            }
+        }
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<VirtualStylusMotionEvent> CREATOR =
+            new Parcelable.Creator<>() {
+                public VirtualStylusMotionEvent createFromParcel(Parcel source) {
+                    return new VirtualStylusMotionEvent(source);
+                }
+                public VirtualStylusMotionEvent[] newArray(int size) {
+                    return new VirtualStylusMotionEvent[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/input/VirtualTouchDeviceConfig.java b/core/java/android/hardware/input/VirtualTouchDeviceConfig.java
new file mode 100644
index 0000000..2e2cfab
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualTouchDeviceConfig.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.os.Parcel;
+
+/**
+ * Configurations to create a virtual touch-based device.
+ *
+ * @hide
+ */
+abstract class VirtualTouchDeviceConfig extends VirtualInputDeviceConfig {
+
+    /** The touch device width. */
+    private final int mWidth;
+    /** The touch device height. */
+    private final int mHeight;
+
+    VirtualTouchDeviceConfig(@NonNull Builder<? extends Builder<?>> builder) {
+        super(builder);
+        mWidth = builder.mWidth;
+        mHeight = builder.mHeight;
+    }
+
+    VirtualTouchDeviceConfig(@NonNull Parcel in) {
+        super(in);
+        mWidth = in.readInt();
+        mHeight = in.readInt();
+    }
+
+    /** Returns the touch device width. */
+    public int getWidth() {
+        return mWidth;
+    }
+
+    /** Returns the touch device height. */
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeInt(mWidth);
+        dest.writeInt(mHeight);
+    }
+
+    @Override
+    @NonNull
+    String additionalFieldsToString() {
+        return " width=" + mWidth + " height=" + mHeight;
+    }
+
+    /**
+     * Builder for creating a {@link VirtualTouchDeviceConfig}.
+     *
+     * @param <T> The subclass to be built.
+     */
+    abstract static class Builder<T extends Builder<T>>
+            extends VirtualInputDeviceConfig.Builder<T> {
+
+        private final int mWidth;
+        private final int mHeight;
+
+        /**
+         * Creates a new instance for the given dimensions of the touch device.
+         *
+         * <p>The dimensions are not pixels but in the screen's raw coordinate space. They do
+         * not necessarily have to correspond to the display size or aspect ratio. In this case the
+         * framework will handle the scaling appropriately.
+         *
+         * @param touchDeviceWidth The width of the touch device.
+         * @param touchDeviceHeight The height of the touch device.
+         */
+        Builder(@IntRange(from = 1) int touchDeviceWidth,
+                @IntRange(from = 1) int touchDeviceHeight) {
+            if (touchDeviceHeight <= 0 || touchDeviceWidth <= 0) {
+                throw new IllegalArgumentException(
+                        "Cannot create a virtual touch-based device, dimensions must be "
+                                + "positive. Got: (" + touchDeviceHeight + ", "
+                                + touchDeviceWidth + ")");
+            }
+            mHeight = touchDeviceHeight;
+            mWidth = touchDeviceWidth;
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
index 6308459..851cee6 100644
--- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -23,38 +23,19 @@
 import android.os.Parcelable;
 
 /**
- * Configurations to create virtual touchscreen.
+ * Configurations to create a virtual touchscreen.
  *
  * @hide
  */
 @SystemApi
-public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable {
-
-    /** The touchscreen width. */
-    private final int mWidth;
-    /** The touchscreen height. */
-    private final int mHeight;
+public final class VirtualTouchscreenConfig extends VirtualTouchDeviceConfig implements Parcelable {
 
     private VirtualTouchscreenConfig(@NonNull Builder builder) {
         super(builder);
-        mWidth = builder.mWidth;
-        mHeight = builder.mHeight;
     }
 
     private VirtualTouchscreenConfig(@NonNull Parcel in) {
         super(in);
-        mWidth = in.readInt();
-        mHeight = in.readInt();
-    }
-
-    /** Returns the touchscreen width. */
-    public int getWidth() {
-        return mWidth;
-    }
-
-    /** Returns the touchscreen height. */
-    public int getHeight() {
-        return mHeight;
     }
 
     @Override
@@ -65,19 +46,11 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
-        dest.writeInt(mWidth);
-        dest.writeInt(mHeight);
-    }
-
-    @Override
-    @NonNull
-    String additionalFieldsToString() {
-        return " width=" + mWidth + " height=" + mHeight;
     }
 
     @NonNull
     public static final Creator<VirtualTouchscreenConfig> CREATOR =
-            new Creator<VirtualTouchscreenConfig>() {
+            new Creator<>() {
                 @Override
                 public VirtualTouchscreenConfig createFromParcel(Parcel in) {
                     return new VirtualTouchscreenConfig(in);
@@ -92,9 +65,7 @@
     /**
      * Builder for creating a {@link VirtualTouchscreenConfig}.
      */
-    public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
-        private int mWidth;
-        private int mHeight;
+    public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> {
 
         /**
          * Creates a new instance for the given dimensions of the {@link VirtualTouchscreen}.
@@ -108,14 +79,7 @@
          */
         public Builder(@IntRange(from = 1) int touchscreenWidth,
                 @IntRange(from = 1) int touchscreenHeight) {
-            if (touchscreenHeight <= 0 || touchscreenWidth <= 0) {
-                throw new IllegalArgumentException(
-                        "Cannot create a virtual touchscreen, touchscreen dimensions must be "
-                                + "positive. Got: (" + touchscreenHeight + ", "
-                                + touchscreenWidth + ")");
-            }
-            mHeight = touchscreenHeight;
-            mWidth = touchscreenWidth;
+            super(touchscreenWidth, touchscreenHeight);
         }
 
         /**
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 561db9c..83b7eda 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -80,8 +80,6 @@
      * <p>The VCN will only migrate to a Carrier WiFi network that has a signal strength greater
      * than, or equal to this threshold.
      *
-     * <p>WARNING: The VCN does not listen for changes to this key made after VCN startup.
-     *
      * @hide
      */
     @NonNull
@@ -94,8 +92,6 @@
      * <p>If the VCN's selected Carrier WiFi network has a signal strength less than this threshold,
      * the VCN will attempt to migrate away from the Carrier WiFi network.
      *
-     * <p>WARNING: The VCN does not listen for changes to this key made after VCN startup.
-     *
      * @hide
      */
     @NonNull
@@ -120,6 +116,15 @@
     public static final String VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY =
             "vcn_network_selection_ipsec_packet_loss_percent_threshold";
 
+    /**
+     * Key for the list of timeouts in minute to stop penalizing an underlying network candidate
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY =
+            "vcn_network_selection_penalty_timeout_minutes_list";
+
     // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz
 
     /**
@@ -168,6 +173,7 @@
                 VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
                 VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
                 VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
+                VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY,
                 VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
                 VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY,
                 VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY,
diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java
index 8510084..f2ef185 100644
--- a/core/java/android/os/BugreportParams.java
+++ b/core/java/android/os/BugreportParams.java
@@ -21,6 +21,7 @@
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.app.admin.flags.Flags;
+import android.compat.annotation.UnsupportedAppUsage;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -131,6 +132,7 @@
      */
     @TestApi
     @FlaggedApi(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED)
+    @UnsupportedAppUsage
     public static final int BUGREPORT_MODE_ONBOARDING = IDumpstate.BUGREPORT_MODE_ONBOARDING;
 
     /**
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index 746278f..e6bfcd7 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -183,13 +183,14 @@
 
         /** @hide */
         @Retention(RetentionPolicy.SOURCE)
-        @IntDef(prefix = {"CPU_LOAD_"}, value = {
+        @IntDef(prefix = {"CPU_LOAD_", "GPU_LOAD_"}, value = {
             CPU_LOAD_UP,
             CPU_LOAD_DOWN,
             CPU_LOAD_RESET,
             CPU_LOAD_RESUME,
             GPU_LOAD_UP,
-            GPU_LOAD_DOWN
+            GPU_LOAD_DOWN,
+            GPU_LOAD_RESET
         })
         public @interface Hint {}
 
diff --git a/core/java/android/os/storage/StorageManagerInternal.java b/core/java/android/os/storage/StorageManagerInternal.java
index 8961846..6995ea8 100644
--- a/core/java/android/os/storage/StorageManagerInternal.java
+++ b/core/java/android/os/storage/StorageManagerInternal.java
@@ -193,7 +193,7 @@
      * @see com.android.server.pm.Installer#createFsveritySetupAuthToken()
      */
     public abstract IInstalld.IFsveritySetupAuthToken createFsveritySetupAuthToken(
-            ParcelFileDescriptor authFd, int appUid, @UserIdInt int userId) throws IOException;
+            ParcelFileDescriptor authFd, int uid) throws IOException;
 
     /**
      * A proxy call to the corresponding method in Installer.
diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl
index 7cecfdc..471f95b 100644
--- a/core/java/android/permission/IPermissionManager.aidl
+++ b/core/java/android/permission/IPermissionManager.aidl
@@ -92,7 +92,7 @@
 
     boolean isAutoRevokeExempted(String packageName, int userId);
 
-    void registerAttributionSource(in AttributionSourceState source);
+    IBinder registerAttributionSource(in AttributionSourceState source);
 
     boolean isRegisteredAttributionSource(in AttributionSourceState source);
 
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 91adc37..4af6e3a 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -23,6 +23,7 @@
 import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED;
 import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
 import static android.os.Build.VERSION_CODES.S;
+import static android.permission.flags.Flags.serverSideAttributionRegistration;
 
 import android.Manifest;
 import android.annotation.CheckResult;
@@ -59,6 +60,7 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
@@ -1464,13 +1466,19 @@
         // We use a shared static token for sources that are not registered since the token's
         // only used for process death detection. If we are about to use the source for security
         // enforcement we need to replace the binder with a unique one.
-        final AttributionSource registeredSource = source.withToken(new Binder());
         try {
-            mPermissionManager.registerAttributionSource(registeredSource.asState());
+            if (serverSideAttributionRegistration()) {
+                IBinder newToken = mPermissionManager.registerAttributionSource(source.asState());
+                return source.withToken(newToken);
+            } else {
+                AttributionSource registeredSource = source.withToken(new Binder());
+                mPermissionManager.registerAttributionSource(registeredSource.asState());
+                return registeredSource;
+            }
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
-        return registeredSource;
+        return source;
     }
 
     /**
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 60143cc..bced217 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -73,6 +73,13 @@
 }
 
 flag {
+    name: "server_side_attribution_registration"
+    namespace: "permissions"
+    description: "controls whether the binder representing an AttributionSource is created in the system server, or client process"
+    bug: "310953959"
+}
+
+flag {
     name: "wallet_role_enabled"
     namespace: "wallet_integration"
     description: "This flag is used to enabled the Wallet Role for all users on the device"
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 47065e1..7d84bb3 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1931,9 +1931,7 @@
      * A matching Activity will only be found if
      * {@link NotificationManager#areAutomaticZenRulesUserManaged()} is true.
      * <p>
-     * Input: Intent's data URI set with an application name, using the "package" schema (like
-     * "package:com.my.app").
-     * Input: The id of the rule, provided in {@link #EXTRA_AUTOMATIC_ZEN_RULE_ID}.
+     * Input: The id of the rule, provided in the {@link #EXTRA_AUTOMATIC_ZEN_RULE_ID} extra.
      * <p>
      * Output: Nothing.
      */
@@ -13476,6 +13474,16 @@
         @Readable
         public static final String OTA_DISABLE_AUTOMATIC_UPDATE = "ota_disable_automatic_update";
 
+
+        /**
+         * Whether to boot with 16K page size compatible kernel
+         * 1 = Boot with 16K kernel
+         * 0 = Boot with 4K kernel (default)
+         * @hide
+         */
+        @Readable
+        public static final String ENABLE_16K_PAGES = "enable_16k_pages";
+
         /** Timeout for package verification.
         * @hide */
         @Readable
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 54248be..d4d602d 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -205,7 +205,7 @@
     private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn";
     private static final String ALLOW_ATT_CONV = "convos";
     private static final String ALLOW_ATT_CONV_FROM = "convosFrom";
-    private static final String ALLOW_ATT_CHANNELS = "channels";
+    private static final String ALLOW_ATT_CHANNELS = "priorityChannels";
     private static final String USER_MODIFIED_FIELDS = "policyUserModifiedFields";
     private static final String DISALLOW_TAG = "disallow";
     private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
@@ -919,9 +919,9 @@
         final int events = safeInt(parser, ALLOW_ATT_EVENTS, ZenPolicy.STATE_UNSET);
         final int reminders = safeInt(parser, ALLOW_ATT_REMINDERS, ZenPolicy.STATE_UNSET);
         if (Flags.modesApi()) {
-            final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.CHANNEL_TYPE_UNSET);
-            if (channels != ZenPolicy.CHANNEL_TYPE_UNSET) {
-                builder.allowChannels(channels);
+            final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET);
+            if (channels != ZenPolicy.STATE_UNSET) {
+                builder.allowPriorityChannels(channels == ZenPolicy.STATE_ALLOW);
                 policySet = true;
             }
             builder.setUserModifiedFields(safeInt(parser, USER_MODIFIED_FIELDS, 0));
@@ -1036,7 +1036,7 @@
                 out);
 
         if (Flags.modesApi()) {
-            writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getAllowedChannels(), out);
+            writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannels(), out);
             out.attributeInt(null, USER_MODIFIED_FIELDS, policy.getUserModifiedFields());
         }
     }
@@ -1053,7 +1053,7 @@
                 out.attributeInt(null, attr, val);
             }
         } else if (Flags.modesApi() && Objects.equals(attr, ALLOW_ATT_CHANNELS)) {
-            if (val != ZenPolicy.CHANNEL_TYPE_UNSET) {
+            if (val != ZenPolicy.STATE_UNSET) {
                 out.attributeInt(null, attr, val);
             }
         } else {
@@ -1238,8 +1238,7 @@
         }
 
         if (Flags.modesApi()) {
-            builder.allowChannels(allowPriorityChannels ? ZenPolicy.CHANNEL_TYPE_PRIORITY
-                    : ZenPolicy.CHANNEL_TYPE_NONE);
+            builder.allowPriorityChannels(allowPriorityChannels);
         }
         return builder.build();
     }
@@ -1369,7 +1368,7 @@
         int state = defaultPolicy.state;
         if (Flags.modesApi()) {
             state = Policy.policyState(defaultPolicy.hasPriorityChannels(),
-                    getAllowPriorityChannelsWithDefault(zenPolicy.getAllowedChannels(),
+                    ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannels(),
                             DEFAULT_ALLOW_PRIORITY_CHANNELS));
         }
 
@@ -1412,24 +1411,6 @@
     }
 
     /**
-     * Gets whether priority channels are permitted by this channel type, with the specified
-     * default if the value is unset. This effectively converts the channel enum to a boolean,
-     * where "true" indicates priority channels are allowed to break through and "false" means
-     * they are not.
-     */
-    public static boolean getAllowPriorityChannelsWithDefault(
-            @ZenPolicy.ChannelType int channelType, boolean defaultAllowChannels) {
-        switch (channelType) {
-            case ZenPolicy.CHANNEL_TYPE_PRIORITY:
-                return true;
-            case ZenPolicy.CHANNEL_TYPE_NONE:
-                return false;
-            default:
-                return defaultAllowChannels;
-        }
-    }
-
-    /**
      * Maps NotificationManager.Policy senders type to ZenPolicy.PeopleType
      */
     public static @ZenPolicy.PeopleType int getZenPolicySenders(int senders) {
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index 8477eb7..2cda7c8 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -184,7 +184,8 @@
     private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET;
     private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET;
     private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET;
-    private @ChannelType int mAllowChannels = CHANNEL_TYPE_UNSET;
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    private @ChannelType int mAllowChannels = CHANNEL_POLICY_UNSET;
     private final @ModifiableField int mUserModifiedFields; // Bitwise representation
 
     /** @hide */
@@ -354,35 +355,34 @@
      */
     public static final int STATE_DISALLOW = 2;
 
-    /** @hide */
-    @IntDef(prefix = { "CHANNEL_TYPE_" }, value = {
-            CHANNEL_TYPE_UNSET,
-            CHANNEL_TYPE_PRIORITY,
-            CHANNEL_TYPE_NONE,
+    @IntDef(prefix = { "CHANNEL_POLICY_" }, value = {
+            CHANNEL_POLICY_UNSET,
+            CHANNEL_POLICY_PRIORITY,
+            CHANNEL_POLICY_NONE,
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface ChannelType {}
+    private @interface ChannelType {}
 
     /**
      * Indicates no explicit setting for which channels may bypass DND when this policy is active.
-     * Defaults to {@link #CHANNEL_TYPE_PRIORITY}.
+     * Defaults to {@link #CHANNEL_POLICY_PRIORITY}.
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
-    public static final int CHANNEL_TYPE_UNSET = 0;
+    private static final int CHANNEL_POLICY_UNSET = 0;
 
     /**
      * Indicates that channels marked as {@link NotificationChannel#canBypassDnd()} can bypass DND
      * when this policy is active.
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
-    public static final int CHANNEL_TYPE_PRIORITY = 1;
+    private static final int CHANNEL_POLICY_PRIORITY = 1;
 
     /**
      * Indicates that no channels can bypass DND when this policy is active, even those marked as
      * {@link NotificationChannel#canBypassDnd()}.
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
-    public static final int CHANNEL_TYPE_NONE = 2;
+    private static final int CHANNEL_POLICY_NONE = 2;
 
     /** @hide */
     public ZenPolicy() {
@@ -584,16 +584,21 @@
     }
 
     /**
-     * Which types of {@link NotificationChannel channels} this policy allows to bypass DND. When
-     * this value is {@link #CHANNEL_TYPE_PRIORITY priority} channels, any channel with
-     * canBypassDnd() may bypass DND; when it is {@link #CHANNEL_TYPE_NONE none}, even channels
-     * with canBypassDnd() will be intercepted.
-     * @return {@link #CHANNEL_TYPE_UNSET}, {@link #CHANNEL_TYPE_PRIORITY}, or
-     *   {@link #CHANNEL_TYPE_NONE}
+     * Whether this policy allows {@link NotificationChannel channels} marked as
+     * {@link NotificationChannel#canBypassDnd()} to bypass DND. If {@link #STATE_ALLOW}, these
+     * channels may bypass; if {@link #STATE_DISALLOW}, then even notifications from channels
+     * with {@link NotificationChannel#canBypassDnd()} will be intercepted.
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
-    public @ChannelType int getAllowedChannels() {
-        return mAllowChannels;
+    public @State int getPriorityChannels() {
+        switch (mAllowChannels) {
+            case CHANNEL_POLICY_PRIORITY:
+                return STATE_ALLOW;
+            case CHANNEL_POLICY_NONE:
+                return STATE_DISALLOW;
+            default:
+                return STATE_UNSET;
+        }
     }
 
     /**
@@ -1016,8 +1021,8 @@
          * Set whether priority channels are permitted to break through DND.
          */
         @FlaggedApi(Flags.FLAG_MODES_API)
-        public @NonNull Builder allowChannels(@ChannelType int channelType) {
-            mZenPolicy.mAllowChannels = channelType;
+        public @NonNull Builder allowPriorityChannels(boolean allow) {
+            mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE;
             return this;
         }
 
@@ -1305,11 +1310,11 @@
     @FlaggedApi(Flags.FLAG_MODES_API)
     public static String channelTypeToString(@ChannelType int channelType) {
         switch (channelType) {
-            case CHANNEL_TYPE_UNSET:
+            case CHANNEL_POLICY_UNSET:
                 return "unset";
-            case CHANNEL_TYPE_PRIORITY:
+            case CHANNEL_POLICY_PRIORITY:
                 return "priority";
-            case CHANNEL_TYPE_NONE:
+            case CHANNEL_POLICY_NONE:
                 return "none";
         }
         return "invalidChannelType{" + channelType + "}";
@@ -1389,11 +1394,11 @@
     }
 
     /** @hide */
-    public boolean isCategoryAllowed(@PriorityCategory int category, boolean defaultVal) {
-        switch (getZenPolicyPriorityCategoryState(category)) {
-            case ZenPolicy.STATE_ALLOW:
+    public static boolean stateToBoolean(@State int state, boolean defaultVal) {
+        switch (state) {
+            case STATE_ALLOW:
                 return true;
-            case ZenPolicy.STATE_DISALLOW:
+            case STATE_DISALLOW:
                 return false;
             default:
                 return defaultVal;
@@ -1401,15 +1406,13 @@
     }
 
     /** @hide */
+    public boolean isCategoryAllowed(@PriorityCategory int category, boolean defaultVal) {
+        return stateToBoolean(getZenPolicyPriorityCategoryState(category), defaultVal);
+    }
+
+    /** @hide */
     public boolean isVisualEffectAllowed(@VisualEffect int effect, boolean defaultVal) {
-        switch (getZenPolicyVisualEffectState(effect)) {
-            case ZenPolicy.STATE_ALLOW:
-                return true;
-            case ZenPolicy.STATE_DISALLOW:
-                return false;
-            default:
-                return defaultVal;
-        }
+        return stateToBoolean(getZenPolicyVisualEffectState(effect), defaultVal);
     }
 
     /**
@@ -1463,8 +1466,8 @@
         // apply allowed channels
         if (Flags.modesApi()) {
             // if no channels are allowed, can't newly allow them
-            if (mAllowChannels != CHANNEL_TYPE_NONE
-                    && policyToApply.mAllowChannels != CHANNEL_TYPE_UNSET) {
+            if (mAllowChannels != CHANNEL_POLICY_NONE
+                    && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) {
                 mAllowChannels = policyToApply.mAllowChannels;
             }
         }
@@ -1530,7 +1533,7 @@
         proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders());
 
         if (Flags.modesApi()) {
-            proto.write(DNDPolicyProto.ALLOW_CHANNELS, getAllowedChannels());
+            proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannels());
         }
 
         proto.flush();
diff --git a/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java b/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java
new file mode 100644
index 0000000..819cc8c
--- /dev/null
+++ b/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.annotation.Nullable;
+
+/**
+ * @hide
+ * @param <DataSourceInstanceType> The type of datasource instance this state applied to.
+ */
+public class CreateIncrementalStateArgs<DataSourceInstanceType extends DataSourceInstance> {
+    private final DataSource<DataSourceInstanceType, Object, Object> mDataSource;
+    private final int mInstanceIndex;
+
+    CreateIncrementalStateArgs(DataSource dataSource, int instanceIndex) {
+        this.mDataSource = dataSource;
+        this.mInstanceIndex = instanceIndex;
+    }
+
+    /**
+     * Gets the datasource instance for this state with a lock.
+     * releaseDataSourceInstanceLocked must be called before this can be called again.
+     * @return The data source instance for this state.
+     *         Null if the datasource instance no longer exists.
+     */
+    public @Nullable DataSourceInstanceType getDataSourceInstanceLocked() {
+        return mDataSource.getDataSourceInstanceLocked(mInstanceIndex);
+    }
+}
diff --git a/core/java/android/tracing/perfetto/CreateTlsStateArgs.java b/core/java/android/tracing/perfetto/CreateTlsStateArgs.java
new file mode 100644
index 0000000..3fad2d1
--- /dev/null
+++ b/core/java/android/tracing/perfetto/CreateTlsStateArgs.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.annotation.Nullable;
+
+/**
+ * @hide
+ * @param <DataSourceInstanceType> The type of datasource instance this state applied to.
+ */
+public class CreateTlsStateArgs<DataSourceInstanceType extends DataSourceInstance> {
+    private final DataSource<DataSourceInstanceType, Object, Object> mDataSource;
+    private final int mInstanceIndex;
+
+    CreateTlsStateArgs(DataSource dataSource, int instanceIndex) {
+        this.mDataSource = dataSource;
+        this.mInstanceIndex = instanceIndex;
+    }
+
+    /**
+     * Gets the datasource instance for this state with a lock.
+     * releaseDataSourceInstanceLocked must be called before this can be called again.
+     * @return The data source instance for this state.
+     *         Null if the datasource instance no longer exists.
+     */
+    public @Nullable DataSourceInstanceType getDataSourceInstanceLocked() {
+        return mDataSource.getDataSourceInstanceLocked(mInstanceIndex);
+    }
+}
diff --git a/core/java/android/tracing/perfetto/DataSource.java b/core/java/android/tracing/perfetto/DataSource.java
new file mode 100644
index 0000000..4e08aee
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSource.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.util.proto.ProtoInputStream;
+
+/**
+ * Templated base class meant to be derived by embedders to create a custom data
+ * source.
+ *
+ * @param <DataSourceInstanceType> The type for the DataSource instances that will be created from
+ *                                 this DataSource type.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public abstract class DataSource<DataSourceInstanceType extends DataSourceInstance,
+        TlsStateType, IncrementalStateType> {
+    protected final long mNativeObj;
+
+    public final String name;
+
+    /**
+     * A function implemented by each datasource to create a new data source instance.
+     *
+     * @param configStream A ProtoInputStream to read the tracing instance's config.
+     * @return A new data source instance setup with the provided config.
+     */
+    public abstract DataSourceInstanceType createInstance(
+            ProtoInputStream configStream, int instanceIndex);
+
+    /**
+     * Constructor for datasource base class.
+     *
+     * @param name The fully qualified name of the datasource.
+     */
+    public DataSource(String name) {
+        this.name = name;
+        this.mNativeObj = nativeCreate(this, name);
+    }
+
+    /**
+     * The main tracing method. Tracing code should call this passing a lambda as
+     * argument, with the following signature: void(TraceContext).
+     * <p>
+     * The lambda will be called synchronously (i.e., always before trace()
+     * returns) only if tracing is enabled and the data source has been enabled in
+     * the tracing config.
+     * <p>
+     * The lambda can be called more than once per trace() call, in the case of
+     * concurrent tracing sessions (or even if the data source is instantiated
+     * twice within the same trace config).
+     *
+     * @param fun The tracing lambda that will be called with the tracing contexts of each active
+     *            tracing instance.
+     */
+    public final void trace(
+            TraceFunction<DataSourceInstanceType, TlsStateType, IncrementalStateType> fun) {
+        nativeTrace(mNativeObj, fun);
+    }
+
+    /**
+     * Flush any trace data from this datasource that has not yet been flushed.
+     */
+    public final void flush() {
+        nativeFlushAll(mNativeObj);
+    }
+
+    /**
+     * Override this method to create a custom TlsState object for your DataSource. A new instance
+     * will be created per trace instance per thread.
+     *
+     * NOTE: Should only be called from native side.
+     */
+    protected TlsStateType createTlsState(CreateTlsStateArgs<DataSourceInstanceType> args) {
+        return null;
+    }
+
+    /**
+     * Override this method to create and use a custom IncrementalState object for your DataSource.
+     *
+     * NOTE: Should only be called from native side.
+     */
+    protected IncrementalStateType createIncrementalState(
+            CreateIncrementalStateArgs<DataSourceInstanceType> args) {
+        return null;
+    }
+
+    /**
+     * Registers the data source on all tracing backends, including ones that
+     * connect after the registration. Doing so enables the data source to receive
+     * Setup/Start/Stop notifications and makes the trace() method work when
+     * tracing is enabled and the data source is selected.
+     * <p>
+     * NOTE: Once registered, we cannot unregister the data source. Therefore, we should avoid
+     * creating and registering data source where not strictly required. This is a fundamental
+     * limitation of Perfetto itself.
+     *
+     * @param params Params to initialize the datasource with.
+     */
+    public void register(DataSourceParams params) {
+        nativeRegisterDataSource(this.mNativeObj, params.bufferExhaustedPolicy);
+    }
+
+    /**
+     * Gets the datasource instance with a specified index.
+     * IMPORTANT: releaseDataSourceInstance must be called after using the datasource instance.
+     * @param instanceIndex The index of the datasource to lock and get.
+     * @return The DataSourceInstance at index instanceIndex.
+     *         Null if the datasource instance at the requested index doesn't exist.
+     */
+    public DataSourceInstanceType getDataSourceInstanceLocked(int instanceIndex) {
+        return (DataSourceInstanceType) nativeGetPerfettoInstanceLocked(mNativeObj, instanceIndex);
+    }
+
+    /**
+     * Unlock the datasource at the specified index.
+     * @param instanceIndex The index of the datasource to unlock.
+     */
+    protected void releaseDataSourceInstance(int instanceIndex) {
+        nativeReleasePerfettoInstanceLocked(mNativeObj, instanceIndex);
+    }
+
+    /**
+     * Called from native side when a new tracing instance starts.
+     *
+     * @param rawConfig byte array of the PerfettoConfig encoded proto.
+     * @return A new Java DataSourceInstance object.
+     */
+    private DataSourceInstanceType createInstance(byte[] rawConfig, int instanceIndex) {
+        final ProtoInputStream inputStream = new ProtoInputStream(rawConfig);
+        return this.createInstance(inputStream, instanceIndex);
+    }
+
+    private static native void nativeRegisterDataSource(
+            long dataSourcePtr, int bufferExhaustedPolicy);
+
+    private static native long nativeCreate(DataSource thiz, String name);
+    private static native void nativeTrace(
+            long nativeDataSourcePointer, TraceFunction traceFunction);
+    private static native void nativeFlushAll(long nativeDataSourcePointer);
+    private static native long nativeGetFinalizer();
+
+    private static native DataSourceInstance nativeGetPerfettoInstanceLocked(
+            long dataSourcePtr, int dsInstanceIdx);
+    private static native void nativeReleasePerfettoInstanceLocked(
+            long dataSourcePtr, int dsInstanceIdx);
+}
diff --git a/core/java/android/tracing/perfetto/DataSourceInstance.java b/core/java/android/tracing/perfetto/DataSourceInstance.java
new file mode 100644
index 0000000..4994501
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSourceInstance.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+/**
+ * @hide
+ */
+public abstract class DataSourceInstance implements AutoCloseable {
+    private final DataSource mDataSource;
+    private final int mInstanceIndex;
+
+    public DataSourceInstance(DataSource dataSource, int instanceIndex) {
+        this.mDataSource = dataSource;
+        this.mInstanceIndex = instanceIndex;
+    }
+
+    /**
+     * Executed when the tracing instance starts running.
+     * <p>
+     * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+     *       Anything that is run in this callback should execute quickly.
+     *
+     * @param args Start arguments.
+     */
+    protected void onStart(StartCallbackArguments args) {}
+
+    /**
+     * Executed when a flush is triggered.
+     * <p>
+     * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+     *       Anything that is run in this callback should execute quickly.
+     * @param args Flush arguments.
+     */
+    protected void onFlush(FlushCallbackArguments args) {}
+
+    /**
+     * Executed when the tracing instance is stopped.
+     * <p>
+     * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+     *       Anything that is run in this callback should execute quickly.
+     * @param args Stop arguments.
+     */
+    protected void onStop(StopCallbackArguments args) {}
+
+    @Override
+    public final void close() {
+        this.release();
+    }
+
+    /**
+     * Release the lock on the datasource once you are finished using it.
+     * Only required to be called when instance was retrieved with
+     * `DataSource#getDataSourceInstanceLocked`.
+     */
+    public final void release() {
+        mDataSource.releaseDataSourceInstance(mInstanceIndex);
+    }
+}
diff --git a/core/java/android/tracing/perfetto/DataSourceParams.java b/core/java/android/tracing/perfetto/DataSourceParams.java
new file mode 100644
index 0000000..6cd04e3
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSourceParams.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * DataSource Parameters
+ *
+ * @hide
+ */
+public class DataSourceParams {
+    /**
+     * @hide
+     */
+    @IntDef(value = {
+        PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP,
+        PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PerfettoDsBufferExhausted {}
+
+    // If the data source runs out of space when trying to acquire a new chunk,
+    // it will drop data.
+    public static final int PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP = 0;
+
+    // If the data source runs out of space when trying to acquire a new chunk,
+    // it will stall, retry and eventually abort if a free chunk is not acquired
+    // after a while.
+    public static final int PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT = 1;
+
+    public static DataSourceParams DEFAULTS =
+            new DataSourceParams(PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP);
+
+    public DataSourceParams(@PerfettoDsBufferExhausted int bufferExhaustedPolicy) {
+        this.bufferExhaustedPolicy = bufferExhaustedPolicy;
+    }
+
+    public final @PerfettoDsBufferExhausted int bufferExhaustedPolicy;
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/tracing/perfetto/FlushCallbackArguments.java
similarity index 81%
rename from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
rename to core/java/android/tracing/perfetto/FlushCallbackArguments.java
index ce92b6d..ecf6aee 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/tracing/perfetto/FlushCallbackArguments.java
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.tracing.perfetto;
 
 /**
- * The configuration of a single virtual camera stream.
  * @hide
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+public class FlushCallbackArguments {
+}
diff --git a/core/java/android/tracing/perfetto/InitArguments.java b/core/java/android/tracing/perfetto/InitArguments.java
new file mode 100644
index 0000000..da8c273
--- /dev/null
+++ b/core/java/android/tracing/perfetto/InitArguments.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @hide
+ */
+public class InitArguments {
+    public final @PerfettoBackend int backends;
+
+    /**
+     * @hide
+     */
+    @IntDef(value = {
+            PERFETTO_BACKEND_IN_PROCESS,
+            PERFETTO_BACKEND_SYSTEM,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PerfettoBackend {}
+
+    // The in-process tracing backend. Keeps trace buffers in the process memory.
+    public static final int PERFETTO_BACKEND_IN_PROCESS = (1 << 0);
+
+    // The system tracing backend. Connects to the system tracing service (e.g.
+    // on Linux/Android/Mac uses a named UNIX socket).
+    public static final int PERFETTO_BACKEND_SYSTEM = (1 << 1);
+
+    public static InitArguments DEFAULTS = new InitArguments(PERFETTO_BACKEND_SYSTEM);
+
+    public static InitArguments TESTING = new InitArguments(PERFETTO_BACKEND_IN_PROCESS);
+
+    public InitArguments(@PerfettoBackend int backends) {
+        this.backends = backends;
+    }
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/tracing/perfetto/Producer.java
similarity index 61%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/tracing/perfetto/Producer.java
index ce92b6d..a1b3eb7 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/tracing/perfetto/Producer.java
@@ -13,10 +13,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.tracing.perfetto;
 
 /**
- * The configuration of a single virtual camera stream.
  * @hide
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+public class Producer {
+
+    /**
+     * Initializes the global Perfetto producer.
+     *
+     * @param args arguments on how to initialize the Perfetto producer.
+     */
+    public static void init(InitArguments args) {
+        nativePerfettoProducerInit(args.backends);
+    }
+
+    private static native void nativePerfettoProducerInit(int backends);
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/tracing/perfetto/StartCallbackArguments.java
similarity index 81%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/tracing/perfetto/StartCallbackArguments.java
index ce92b6d..9739d27 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/tracing/perfetto/StartCallbackArguments.java
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.tracing.perfetto;
 
 /**
- * The configuration of a single virtual camera stream.
  * @hide
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+public class StartCallbackArguments {
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/tracing/perfetto/StopCallbackArguments.java
similarity index 81%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/tracing/perfetto/StopCallbackArguments.java
index ce92b6d..0cd1a18 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/tracing/perfetto/StopCallbackArguments.java
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.tracing.perfetto;
 
 /**
- * The configuration of a single virtual camera stream.
  * @hide
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+public class StopCallbackArguments {
+}
diff --git a/core/java/android/tracing/perfetto/TraceFunction.java b/core/java/android/tracing/perfetto/TraceFunction.java
new file mode 100644
index 0000000..62941df
--- /dev/null
+++ b/core/java/android/tracing/perfetto/TraceFunction.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import java.io.IOException;
+
+/**
+ * The interface for the trace function called from native on a trace call with a context.
+ *
+ * @param <DataSourceInstanceType> The type of DataSource this tracing context is for.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public interface TraceFunction<DataSourceInstanceType extends DataSourceInstance,
+        TlsStateType, IncrementalStateType> {
+
+    /**
+     * This function will be called synchronously (i.e., always before trace() returns) only if
+     * tracing is enabled and the data source has been enabled in the tracing config.
+     * It can be called more than once per trace() call, in the case of concurrent tracing sessions
+     * (or even if the data source is instantiated twice within the same trace config).
+     *
+     * @param ctx the tracing context to trace for in the trace function.
+     */
+    void trace(TracingContext<DataSourceInstanceType, TlsStateType, IncrementalStateType> ctx)
+            throws IOException;
+}
diff --git a/core/java/android/tracing/perfetto/TracingContext.java b/core/java/android/tracing/perfetto/TracingContext.java
new file mode 100644
index 0000000..0bce26e
--- /dev/null
+++ b/core/java/android/tracing/perfetto/TracingContext.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.util.proto.ProtoOutputStream;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Argument passed to the lambda function passed to Trace().
+ *
+ * @param <DataSourceInstanceType> The type of the datasource this tracing context is for.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public class TracingContext<DataSourceInstanceType extends DataSourceInstance,
+        TlsStateType, IncrementalStateType> {
+
+    private final long mContextPtr;
+    private final TlsStateType mTlsState;
+    private final IncrementalStateType mIncrementalState;
+    private final List<ProtoOutputStream> mTracePackets = new ArrayList<>();
+
+    // Should only be created from the native side.
+    private TracingContext(long contextPtr, TlsStateType tlsState,
+            IncrementalStateType incrementalState) {
+        this.mContextPtr = contextPtr;
+        this.mTlsState = tlsState;
+        this.mIncrementalState = incrementalState;
+    }
+
+    /**
+     * Creates a new output stream to be used to write a trace packet to. The output stream will be
+     * encoded to the proto binary representation when the callback trace function finishes and
+     * send over to the native side to be included in the proto buffer.
+     *
+     * @return A proto output stream to write a trace packet proto to
+     */
+    public ProtoOutputStream newTracePacket() {
+        final ProtoOutputStream os = new ProtoOutputStream(0);
+        mTracePackets.add(os);
+        return os;
+    }
+
+    /**
+     * Forces a commit of the thread-local tracing data written so far to the
+     * service. This is almost never required (tracing data is periodically
+     * committed as trace pages are filled up) and has a non-negligible
+     * performance hit (requires an IPC + refresh of the current thread-local
+     * chunk). The only case when this should be used is when handling OnStop()
+     * asynchronously, to ensure sure that the data is committed before the
+     * Stop timeout expires.
+     */
+    public void flush() {
+        nativeFlush(this, mContextPtr);
+    }
+
+    /**
+     * Can optionally be used to store custom per-sequence
+     * session data, which is not reset when incremental state is cleared
+     * (e.g. configuration options).
+     *
+     * @return The TlsState instance for the tracing thread and instance.
+     */
+    public TlsStateType getCustomTlsState() {
+        return this.mTlsState;
+    }
+
+    /**
+     * Can optionally be used store custom per-sequence
+     * incremental data (e.g., interning tables).
+     *
+     * @return The current IncrementalState object instance.
+     */
+    public IncrementalStateType getIncrementalState() {
+        return this.mIncrementalState;
+    }
+
+    // Called from native to get trace packets
+    private byte[][] getAndClearAllPendingTracePackets() {
+        byte[][] res = new byte[mTracePackets.size()][];
+        for (int i = 0; i < mTracePackets.size(); i++) {
+            ProtoOutputStream tracePacket = mTracePackets.get(i);
+            res[i] = tracePacket.getBytes();
+        }
+
+        mTracePackets.clear();
+        return res;
+    }
+
+    // private static native void nativeFlush(long nativeDataSourcePointer);
+    private static native void nativeFlush(TracingContext thiz, long ctxPointer);
+}
diff --git a/core/java/android/tracing/transition/TransitionDataSource.java b/core/java/android/tracing/transition/TransitionDataSource.java
new file mode 100644
index 0000000..b8daace
--- /dev/null
+++ b/core/java/android/tracing/transition/TransitionDataSource.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.transition;
+
+import android.tracing.perfetto.DataSource;
+import android.tracing.perfetto.DataSourceInstance;
+import android.tracing.perfetto.FlushCallbackArguments;
+import android.tracing.perfetto.StartCallbackArguments;
+import android.tracing.perfetto.StopCallbackArguments;
+import android.util.proto.ProtoInputStream;
+
+/**
+ * @hide
+ */
+public class TransitionDataSource extends DataSource {
+    public static String DATA_SOURCE_NAME = "com.android.wm.shell.transition";
+
+    private final Runnable mOnStartStaticCallback;
+    private final Runnable mOnFlushStaticCallback;
+    private final Runnable mOnStopStaticCallback;
+
+    public TransitionDataSource(Runnable onStart, Runnable onFlush, Runnable onStop) {
+        super(DATA_SOURCE_NAME);
+        this.mOnStartStaticCallback = onStart;
+        this.mOnFlushStaticCallback = onFlush;
+        this.mOnStopStaticCallback = onStop;
+    }
+
+    @Override
+    public DataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) {
+        return new DataSourceInstance(this, instanceIndex) {
+            @Override
+            protected void onStart(StartCallbackArguments args) {
+                mOnStartStaticCallback.run();
+            }
+
+            @Override
+            protected void onFlush(FlushCallbackArguments args) {
+                mOnFlushStaticCallback.run();
+            }
+
+            @Override
+            protected void onStop(StopCallbackArguments args) {
+                mOnStopStaticCallback.run();
+            }
+        };
+    }
+}
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index bb7677d6..ccc5dbb 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -47,3 +47,10 @@
     is_fixed_read_only: true
 }
 
+flag {
+    name: "use_zero_jank_proxy"
+    namespace: "input_method"
+    description: "Feature flag for using a proxy that uses async calls to achieve zero jank for IMMS calls."
+    bug: "293640003"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java
index 828ec26..c6271d2 100644
--- a/core/java/android/webkit/URLUtil.java
+++ b/core/java/android/webkit/URLUtil.java
@@ -16,20 +16,40 @@
 
 package android.webkit;
 
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.compat.Compatibility;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.net.ParseException;
 import android.net.Uri;
 import android.net.WebAddress;
+import android.os.Build;
 import android.util.Log;
 
 import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public final class URLUtil {
 
+    /**
+     * This feature enables parsing of Content-Disposition headers that conform to RFC 6266. In
+     * particular, this enables parsing of {@code filename*} values which can use a different
+     * character encoding.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @FlaggedApi(android.os.Flags.FLAG_ANDROID_OS_BUILD_VANILLA_ICE_CREAM)
+    public static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L;
+
     private static final String LOGTAG = "webkit";
     private static final boolean TRACE = false;
 
@@ -293,21 +313,58 @@
 
     /**
      * Guesses canonical filename that a download would have, using the URL and contentDisposition.
-     * File extension, if not defined, is added based on the mimetype
+     *
+     * <p>File extension, if not defined, is added based on the mimetype.
+     *
+     * <p>The {@code contentDisposition} argument will be treated differently depending on
+     * targetSdkVersion.
+     *
+     * <ul>
+     *   <li>For targetSDK versions &lt; {@code VANILLA_ICE_CREAM} it will be parsed based on RFC
+     *       2616.
+     *   <li>For targetSDK versions &gt;= {@code VANILLA_ICE_CREAM} it will be parsed based on RFC
+     *       6266.
+     * </ul>
+     *
+     * In practice, this means that from {@code VANILLA_ICE_CREAM}, this method will be able to
+     * parse {@code filename*} directives in the {@code contentDisposition} string.
+     *
+     * <p>The function also changed in the following ways in {@code VANILLA_ICE_CREAM}:
+     *
+     * <ul>
+     *   <li>If the suggested file type extension doesn't match the passed {@code mimeType}, the
+     *       method will append the appropriate extension instead of replacing the current
+     *       extension.
+     *   <li>If the suggested file name contains a path separator ({@code "/"}), the method will
+     *       replace this with the underscore character ({@code "_"}) instead of splitting the
+     *       result and only using the last part.
+     * </ul>
      *
      * @param url Url to the content
      * @param contentDisposition Content-Disposition HTTP header or {@code null}
      * @param mimeType Mime-type of the content or {@code null}
      * @return suggested filename
      */
-    public static final String guessFileName(
+    public static String guessFileName(
+            String url, @Nullable String contentDisposition, @Nullable String mimeType) {
+        if (android.os.Flags.androidOsBuildVanillaIceCream()) {
+            if (Compatibility.isChangeEnabled(PARSE_CONTENT_DISPOSITION_USING_RFC_6266)) {
+                return guessFileNameRfc6266(url, contentDisposition, mimeType);
+            }
+        }
+
+        return guessFileNameRfc2616(url, contentDisposition, mimeType);
+    }
+
+    /** Legacy implementation of guessFileName, based on RFC 2616. */
+    private static String guessFileNameRfc2616(
             String url, @Nullable String contentDisposition, @Nullable String mimeType) {
         String filename = null;
         String extension = null;
 
         // If we couldn't do anything with the hint, move toward the content disposition
         if (contentDisposition != null) {
-            filename = parseContentDisposition(contentDisposition);
+            filename = parseContentDispositionRfc2616(contentDisposition);
             if (filename != null) {
                 int index = filename.lastIndexOf('/') + 1;
                 if (index > 0) {
@@ -384,6 +441,128 @@
         return filename + extension;
     }
 
+    /**
+     * Guesses canonical filename that a download would have, using the URL and contentDisposition.
+     * Uses RFC 6266 for parsing the contentDisposition header value.
+     */
+    @NonNull
+    private static String guessFileNameRfc6266(
+            @NonNull String url, @Nullable String contentDisposition, @Nullable String mimeType) {
+        String filename = getFilenameSuggestion(url, contentDisposition);
+        // Split filename between base and extension
+        // Add an extension if filename does not have one
+        String extensionFromMimeType = suggestExtensionFromMimeType(mimeType);
+
+        if (filename.indexOf('.') < 0) {
+            // Filename does not have an extension, use the suggested one.
+            return filename + extensionFromMimeType;
+        }
+
+        // Filename already contains at least one dot.
+        // Compare the last segment of the extension against the mime type.
+        // If there's a mismatch, add the suggested extension instead.
+        if (mimeType != null && extensionDifferentFromMimeType(filename, mimeType)) {
+            return filename + extensionFromMimeType;
+        }
+        return filename;
+    }
+
+    /**
+     * Get the suggested file name from the {@code contentDisposition} or {@code url}. Will ensure
+     * that the filename contains no path separators by replacing them with the {@code "_"}
+     * character.
+     */
+    @NonNull
+    private static String getFilenameSuggestion(String url, @Nullable String contentDisposition) {
+        // First attempt to parse the Content-Disposition header if available
+        if (contentDisposition != null) {
+            String filename = getFilenameFromContentDispositionRfc6266(contentDisposition);
+            if (filename != null) {
+                return replacePathSeparators(filename);
+            }
+        }
+
+        // Try to generate a filename based on the URL.
+        if (url != null) {
+            Uri parsedUri = Uri.parse(url);
+            String lastPathSegment = parsedUri.getLastPathSegment();
+            if (lastPathSegment != null) {
+                return replacePathSeparators(lastPathSegment);
+            }
+        }
+
+        // Finally, if couldn't get filename from URI, get a generic filename.
+        return "downloadfile";
+    }
+
+    /**
+     * Replace all instances of {@code "/"} with {@code "_"} to avoid filenames that navigate the
+     * path.
+     */
+    @NonNull
+    private static String replacePathSeparators(@NonNull String raw) {
+        return raw.replaceAll("/", "_");
+    }
+
+    /**
+     * Check if the {@code filename} has an extension that is different from the expected one based
+     * on the {@code mimeType}.
+     */
+    private static boolean extensionDifferentFromMimeType(
+            @NonNull String filename, @NonNull String mimeType) {
+        int lastDotIndex = filename.lastIndexOf('.');
+        String typeFromExt =
+                MimeTypeMap.getSingleton()
+                        .getMimeTypeFromExtension(filename.substring(lastDotIndex + 1));
+        return typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType);
+    }
+
+    /**
+     * Get a candidate file extension (including the {@code .}) for the given mimeType. will return
+     * {@code ".bin"} if {@code mimeType} is {@code null}
+     *
+     * @param mimeType Reported mimetype
+     * @return A file extension, including the {@code .}
+     */
+    @NonNull
+    private static String suggestExtensionFromMimeType(@Nullable String mimeType) {
+        if (mimeType == null) {
+            return ".bin";
+        }
+        String extensionFromMimeType =
+                MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+        if (extensionFromMimeType != null) {
+            return "." + extensionFromMimeType;
+        }
+        if (mimeType.equalsIgnoreCase("text/html")) {
+            return ".html";
+        } else if (mimeType.toLowerCase(Locale.ROOT).startsWith("text/")) {
+            return ".txt";
+        } else {
+            return ".bin";
+        }
+    }
+
+    /**
+     * Parse the Content-Disposition HTTP Header.
+     *
+     * <p>Behavior depends on targetSdkVersion.
+     *
+     * <ul>
+     *   <li>For targetSDK versions &lt; {@code VANILLA_ICE_CREAM} it will parse based on RFC 2616.
+     *   <li>For targetSDK versions &gt;= {@code VANILLA_ICE_CREAM} it will parse based on RFC 6266.
+     * </ul>
+     */
+    @UnsupportedAppUsage
+    static String parseContentDisposition(String contentDisposition) {
+        if (android.os.Flags.androidOsBuildVanillaIceCream()) {
+            if (Compatibility.isChangeEnabled(PARSE_CONTENT_DISPOSITION_USING_RFC_6266)) {
+                return getFilenameFromContentDispositionRfc6266(contentDisposition);
+            }
+        }
+        return parseContentDispositionRfc2616(contentDisposition);
+    }
+
     /** Regex used to parse content-disposition headers */
     private static final Pattern CONTENT_DISPOSITION_PATTERN =
             Pattern.compile(
@@ -391,15 +570,14 @@
                     Pattern.CASE_INSENSITIVE);
 
     /**
-     * Parse the Content-Disposition HTTP Header. The format of the header is defined here:
-     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
-     * content that is going to be downloaded to the file system. We only support the attachment
-     * type. Note that RFC 2616 specifies the filename value must be double-quoted. Unfortunately
-     * some servers do not quote the value so to maintain consistent behaviour with other browsers,
-     * we allow unquoted values too.
+     * Parse the Content-Disposition HTTP Header. The format of the header is defined here: <a
+     * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html">rfc2616 Section 19</a>. This
+     * header provides a filename for content that is going to be downloaded to the file system. We
+     * only support the attachment type. Note that RFC 2616 specifies the filename value must be
+     * double-quoted. Unfortunately some servers do not quote the value so to maintain consistent
+     * behaviour with other browsers, we allow unquoted values too.
      */
-    @UnsupportedAppUsage
-    static String parseContentDisposition(String contentDisposition) {
+    private static String parseContentDispositionRfc2616(String contentDisposition) {
         try {
             Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
             if (m.find()) {
@@ -410,4 +588,136 @@
         }
         return null;
     }
+
+    /**
+     * Pattern for parsing individual content disposition key-value pairs.
+     *
+     * <p>The pattern will attempt to parse the value as either single-, double-, or unquoted. For
+     * the single- and double-quoted options, the pattern allows escaped quotes as part of the
+     * value, as per <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-2.2">rfc2616
+     * section-2.2</a>
+     */
+    @SuppressWarnings("RegExpRepeatedSpace") // Spaces are only for readability.
+    private static final Pattern DISPOSITION_PATTERN =
+            Pattern.compile(
+                    """
+                            \\s*(\\S+?) # Group 1: parameter name
+                            \\s*=\\s* # Match equals sign
+                            (?: # non-capturing group of options
+                               '( (?: [^'\\\\] | \\\\. )* )' # Group 2: single-quoted
+                             | "( (?: [^"\\\\] | \\\\. )*  )" # Group 3: double-quoted
+                             | ( [^'"][^;\\s]* ) # Group 4: un-quoted parameter
+                            )\\s*;? # Optional end semicolon""",
+                    Pattern.COMMENTS);
+
+    /**
+     * Extract filename from a {@code Content-Disposition} header value.
+     *
+     * <p>This method implements the parsing defined in <a
+     * href="https://datatracker.ietf.org/doc/html/rfc6266">RFC 6266</a>, supporting both the {@code
+     * filename} and {@code filename*} disposition parameters. If the passed header value has the
+     * {@code "inline"} disposition type, this method will return {@code null} to indicate that a
+     * download was not intended.
+     *
+     * <p>If both {@code filename*} and {@code filename} is present, the former will be returned, as
+     * per the RFC. Invalid encoded values will be ignored.
+     *
+     * @param contentDisposition Value of {@code Content-Disposition} header.
+     * @return The filename suggested by the header or {@code null} if no filename could be parsed
+     *     from the header value.
+     */
+    @Nullable
+    private static String getFilenameFromContentDispositionRfc6266(
+            @NonNull String contentDisposition) {
+        String[] parts = contentDisposition.trim().split(";", 2);
+        if (parts.length < 2) {
+            // Need at least 2 parts, the `disposition-type` and at least one `disposition-parm`.
+            return null;
+        }
+        String dispositionType = parts[0].trim();
+        if ("inline".equalsIgnoreCase(dispositionType)) {
+            // "inline" should not result in a download.
+            // Unknown disposition types should be handles as "attachment"
+            // https://datatracker.ietf.org/doc/html/rfc6266#section-4.2
+            return null;
+        }
+        String dispositionParameters = parts[1];
+        Matcher matcher = DISPOSITION_PATTERN.matcher(dispositionParameters);
+        String filename = null;
+        String filenameExt = null;
+        while (matcher.find()) {
+            String parameter = matcher.group(1);
+            String value;
+            if (matcher.group(2) != null) {
+                value = removeSlashEscapes(matcher.group(2)); // Value was single-quoted
+            } else if (matcher.group(3) != null) {
+                value = removeSlashEscapes(matcher.group(3)); // Value was double-quoted
+            } else {
+                value = matcher.group(4); // Value was un-quoted
+            }
+
+            if (parameter == null || value == null) {
+                continue;
+            }
+
+            if ("filename*".equalsIgnoreCase(parameter)) {
+                filenameExt = parseExtValueString(value);
+            } else if ("filename".equalsIgnoreCase(parameter)) {
+                filename = value;
+            }
+        }
+
+        // RFC 6266 dictates the filenameExt should be preferred if present.
+        if (filenameExt != null) {
+            return filenameExt;
+        }
+        return filename;
+    }
+
+    /** Replace escapes of the \X form with X. */
+    private static String removeSlashEscapes(String raw) {
+        if (raw == null) {
+            return null;
+        }
+        return raw.replaceAll("\\\\(.)", "$1");
+    }
+
+    /**
+     * Parse an extended value string which can be percent-encoded. Return {@code} null if unable to
+     * parse the string.
+     */
+    private static String parseExtValueString(String raw) {
+        String[] parts = raw.split("'", 3);
+        if (parts.length < 3) {
+            return null;
+        }
+
+        String encoding = parts[0];
+        // Intentionally ignore parts[1] (language).
+        String valueChars = parts[2];
+
+        try {
+            // The URLDecoder force-decodes + as " "
+            // so preemptively replace all values with the encoded value to preserve them.
+            Charset charset = Charset.forName(encoding);
+            String valueWithEncodedPlus = encodePlusCharacters(valueChars, charset);
+            return URLDecoder.decode(valueWithEncodedPlus, charset);
+        } catch (RuntimeException ignored) {
+            return null; // Ignoring an un-parsable value is within spec.
+        }
+    }
+
+    /**
+     * Replace all instances of {@code "+"} with the percent-encoded equivalent for the given {@code
+     * charset}.
+     */
+    @NonNull
+    private static String encodePlusCharacters(@NonNull String valueChars, Charset charset) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : charset.encode("+").array()) {
+            // Formatting a byte is not possible with TextUtils.formatSimple
+            sb.append(String.format("%02x", b));
+        }
+        return valueChars.replaceAll("\\+", sb.toString());
+    }
 }
diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java
index 0ef37d1..53b047a 100644
--- a/core/java/android/webkit/WebViewFactory.java
+++ b/core/java/android/webkit/WebViewFactory.java
@@ -16,6 +16,8 @@
 
 package android.webkit;
 
+import static android.webkit.Flags.updateServiceV2;
+
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.annotation.UptimeMillisLong;
@@ -33,6 +35,7 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.text.TextUtils;
 import android.util.AndroidRuntimeException;
 import android.util.ArraySet;
 import android.util.Log;
@@ -410,6 +413,21 @@
         }
     }
 
+    // Returns whether the given package is enabled.
+    // This state can be changed by the user from Settings->Apps
+    private static boolean isEnabledPackage(PackageInfo packageInfo) {
+        if (packageInfo == null) return false;
+        return packageInfo.applicationInfo.enabled;
+    }
+
+    // Return {@code true} if the package is installed and not hidden
+    private static boolean isInstalledPackage(PackageInfo packageInfo) {
+        if (packageInfo == null) return false;
+        return (((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0)
+                && ((packageInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_HIDDEN)
+                        == 0));
+    }
+
     @UnsupportedAppUsage
     private static Context getWebViewContextAndSetProvider() throws MissingWebViewPackageException {
         Application initialApplication = AppGlobals.getInitialApplication();
@@ -456,6 +474,21 @@
                 Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
             }
 
+            if (updateServiceV2() && !isInstalledPackage(newPackageInfo)) {
+                throw new MissingWebViewPackageException(
+                        TextUtils.formatSimple(
+                                "Current WebView Package (%s) is not installed for the current "
+                                + "user",
+                                newPackageInfo.packageName));
+            }
+
+            if (updateServiceV2() && !isEnabledPackage(newPackageInfo)) {
+                throw new MissingWebViewPackageException(
+                        TextUtils.formatSimple(
+                                "Current WebView Package (%s) is not enabled for the current user",
+                                newPackageInfo.packageName));
+            }
+
             // Validate the newly fetched package info, throws MissingWebViewPackageException on
             // failure
             verifyPackageInfo(response.packageInfo, newPackageInfo);
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index acc6a74..7b8cdff 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -125,6 +125,16 @@
      */
     public static final int OP_TYPE_SET_DIM_ON_TASK = 16;
 
+    /**
+     * Sets this TaskFragment to move to bottom of the Task if any of the activities below it is
+     * launched in a mode requiring clear top.
+     *
+     * This is only allowed for system organizers. See
+     * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer(
+     * ITaskFragmentOrganizer, boolean)}
+     */
+    public static final int OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH = 17;
+
     @IntDef(prefix = { "OP_TYPE_" }, value = {
             OP_TYPE_UNKNOWN,
             OP_TYPE_CREATE_TASK_FRAGMENT,
@@ -144,6 +154,7 @@
             OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE,
             OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE,
             OP_TYPE_SET_DIM_ON_TASK,
+            OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface OperationType {}
@@ -173,12 +184,14 @@
 
     private final boolean mDimOnTask;
 
+    private final boolean mMoveToBottomIfClearWhenLaunch;
+
     private TaskFragmentOperation(@OperationType int opType,
             @Nullable TaskFragmentCreationParams taskFragmentCreationParams,
             @Nullable IBinder activityToken, @Nullable Intent activityIntent,
             @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken,
             @Nullable TaskFragmentAnimationParams animationParams,
-            boolean isolatedNav, boolean dimOnTask) {
+            boolean isolatedNav, boolean dimOnTask, boolean moveToBottomIfClearWhenLaunch) {
         mOpType = opType;
         mTaskFragmentCreationParams = taskFragmentCreationParams;
         mActivityToken = activityToken;
@@ -188,6 +201,7 @@
         mAnimationParams = animationParams;
         mIsolatedNav = isolatedNav;
         mDimOnTask = dimOnTask;
+        mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch;
     }
 
     private TaskFragmentOperation(Parcel in) {
@@ -200,6 +214,7 @@
         mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR);
         mIsolatedNav = in.readBoolean();
         mDimOnTask = in.readBoolean();
+        mMoveToBottomIfClearWhenLaunch = in.readBoolean();
     }
 
     @Override
@@ -213,6 +228,7 @@
         dest.writeTypedObject(mAnimationParams, flags);
         dest.writeBoolean(mIsolatedNav);
         dest.writeBoolean(mDimOnTask);
+        dest.writeBoolean(mMoveToBottomIfClearWhenLaunch);
     }
 
     @NonNull
@@ -300,6 +316,14 @@
         return mDimOnTask;
     }
 
+    /**
+     * Returns whether the TaskFragment should move to bottom of task when any activity below it
+     * is launched in clear top mode.
+     */
+    public boolean isMoveToBottomIfClearWhenLaunch() {
+        return mMoveToBottomIfClearWhenLaunch;
+    }
+
     @Override
     public String toString() {
         final StringBuilder sb = new StringBuilder();
@@ -324,6 +348,7 @@
         }
         sb.append(", isolatedNav=").append(mIsolatedNav);
         sb.append(", dimOnTask=").append(mDimOnTask);
+        sb.append(", moveToBottomIfClearWhenLaunch=").append(mMoveToBottomIfClearWhenLaunch);
 
         sb.append('}');
         return sb.toString();
@@ -332,7 +357,8 @@
     @Override
     public int hashCode() {
         return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent,
-                mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask);
+                mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask,
+                mMoveToBottomIfClearWhenLaunch);
     }
 
     @Override
@@ -349,7 +375,8 @@
                 && Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken)
                 && Objects.equals(mAnimationParams, other.mAnimationParams)
                 && mIsolatedNav == other.mIsolatedNav
-                && mDimOnTask == other.mDimOnTask;
+                && mDimOnTask == other.mDimOnTask
+                && mMoveToBottomIfClearWhenLaunch == other.mMoveToBottomIfClearWhenLaunch;
     }
 
     @Override
@@ -385,6 +412,8 @@
 
         private boolean mDimOnTask;
 
+        private boolean mMoveToBottomIfClearWhenLaunch;
+
         /**
          * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}.
          */
@@ -466,13 +495,23 @@
         }
 
         /**
+         * Sets whether the TaskFragment should move to bottom of task when any activity below it
+         * is launched in clear top mode.
+         */
+        @NonNull
+        public Builder setMoveToBottomIfClearWhenLaunch(boolean moveToBottomIfClearWhenLaunch) {
+            mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch;
+            return this;
+        }
+
+        /**
          * Constructs the {@link TaskFragmentOperation}.
          */
         @NonNull
         public TaskFragmentOperation build() {
             return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken,
                     mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams,
-                    mIsolatedNav, mDimOnTask);
+                    mIsolatedNav, mDimOnTask, mMoveToBottomIfClearWhenLaunch);
         }
     }
 }
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index 4b5595f..cbf6367 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -43,3 +43,10 @@
   description: "Whether the API PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT is available"
   bug: "310816437"
 }
+
+flag {
+  name: "allow_hide_scm_button"
+  namespace: "large_screen_experiences_app_compat"
+  description: "Whether we should allow hiding the size compat restart button"
+  bug: "318840081"
+}
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index 4fe9aea..85bdbb9 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -80,5 +80,11 @@
             in Bundle extras, in IntentSender resultIntent);
     boolean isRequestPinAppWidgetSupported();
     oneway void noteAppWidgetTapped(in String callingPackage, in int appWidgetId);
+    void setWidgetPreview(in ComponentName providerComponent, in int widgetCategories,
+            in RemoteViews preview);
+    @nullable RemoteViews getWidgetPreview(in String callingPackage,
+            in ComponentName providerComponent, in int profileId, in int widgetCategory);
+    void removeWidgetPreview(in ComponentName providerComponent, in int widgetCategories);
+
 }
 
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
index 12aff1c..5d82d04 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
@@ -29,7 +29,6 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
-import android.multiuser.Flags;
 import android.os.Build;
 import android.os.PatternMatcher;
 import android.util.Slog;
@@ -127,10 +126,6 @@
                     .setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SINGLE_USER,
                             R.styleable.AndroidManifestProvider_singleUser, sa));
 
-            if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
-                provider.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SYSTEM_USER_ONLY,
-                        R.styleable.AndroidManifestProvider_systemUserOnly, sa));
-            }
             visibleToEphemeral = sa.getBoolean(
                     R.styleable.AndroidManifestProvider_visibleToInstantApps, false);
             if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
index 4ac542f8..a1dd19a3 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
@@ -29,7 +29,6 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
-import android.multiuser.Flags;
 import android.os.Build;
 
 import com.android.internal.R;
@@ -106,11 +105,6 @@
                             | flag(ServiceInfo.FLAG_SINGLE_USER,
                             R.styleable.AndroidManifestService_singleUser, sa)));
 
-            if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
-                service.setFlags(service.getFlags() | flag(ServiceInfo.FLAG_SYSTEM_USER_ONLY,
-                        R.styleable.AndroidManifestService_systemUserOnly, sa));
-            }
-
             visibleToEphemeral = sa.getBoolean(
                     R.styleable.AndroidManifestService_visibleToInstantApps, false);
             if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index 58ee2b2..13efaf0 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -26,6 +26,8 @@
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK;
@@ -234,6 +236,19 @@
      */
     public static final int ACTION_BACK_SYSTEM_ANIMATION = 25;
 
+    /**
+     * Time notifications spent in hidden state for performance reasons. We might temporary
+     * hide notifications after display size changes (e.g. fold/unfold of a foldable device)
+     * and measure them while they are hidden to unblock rendering of the rest of the UI.
+     */
+    public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE = 26;
+
+    /**
+     * The same as {@link ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE} but tracks time only
+     * when the notifications are hidden and when the shade is open or keyguard is visible.
+     */
+    public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN = 27;
+
     private static final int[] ACTIONS_ALL = {
         ACTION_EXPAND_PANEL,
         ACTION_TOGGLE_RECENTS,
@@ -261,6 +276,8 @@
         ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
         ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
         ACTION_BACK_SYSTEM_ANIMATION,
+        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
     };
 
     /** @hide */
@@ -291,6 +308,8 @@
         ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
         ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
         ACTION_BACK_SYSTEM_ANIMATION,
+        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Action {
@@ -324,6 +343,8 @@
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
     };
 
     private final Object mLock = new Object();
@@ -514,6 +535,10 @@
                 return "ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME";
             case UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION:
                 return "ACTION_BACK_SYSTEM_ANIMATION";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE:
+                return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN:
+                return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN";
             default:
                 throw new IllegalArgumentException("Invalid action");
         }
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 2a744e3..c8fd246 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -269,6 +269,9 @@
                 "android_window_WindowInfosListener.cpp",
                 "android_window_ScreenCapture.cpp",
                 "jni_common.cpp",
+                "android_tracing_PerfettoDataSource.cpp",
+                "android_tracing_PerfettoDataSourceInstance.cpp",
+                "android_tracing_PerfettoProducer.cpp",
             ],
 
             static_libs: [
@@ -282,6 +285,7 @@
                 "libscrypt_static",
                 "libstatssocket_lazy",
                 "libskia",
+                "libperfetto_client_experimental",
             ],
 
             shared_libs: [
@@ -355,6 +359,7 @@
                 "server_configurable_flags",
                 "libimage_io",
                 "libultrahdr",
+                "libperfetto_c",
             ],
             export_shared_lib_headers: [
                 // our headers include libnativewindow's public headers
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 17aad43..7a16318 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -220,6 +220,9 @@
 extern int register_android_window_WindowInfosListener(JNIEnv* env);
 extern int register_android_window_ScreenCapture(JNIEnv* env);
 extern int register_jni_common(JNIEnv* env);
+extern int register_android_tracing_PerfettoDataSource(JNIEnv* env);
+extern int register_android_tracing_PerfettoDataSourceInstance(JNIEnv* env);
+extern int register_android_tracing_PerfettoProducer(JNIEnv* env);
 
 // Namespace for Android Runtime flags applied during boot time.
 static const char* RUNTIME_NATIVE_BOOT_NAMESPACE = "runtime_native_boot";
@@ -1675,6 +1678,10 @@
         REG_JNI(register_android_window_WindowInfosListener),
         REG_JNI(register_android_window_ScreenCapture),
         REG_JNI(register_jni_common),
+
+        REG_JNI(register_android_tracing_PerfettoDataSource),
+        REG_JNI(register_android_tracing_PerfettoDataSourceInstance),
+        REG_JNI(register_android_tracing_PerfettoProducer),
 };
 
 /*
diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp
new file mode 100644
index 0000000..d710698
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSource.cpp
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include "android_tracing_PerfettoDataSource.h"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+static struct {
+    jclass clazz;
+    jmethodID createInstance;
+    jmethodID createTlsState;
+    jmethodID createIncrementalState;
+} gPerfettoDataSourceClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+    jmethodID getAndClearAllPendingTracePackets;
+} gTracingContextClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gCreateTlsStateArgsClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gCreateIncrementalStateArgsClassInfo;
+
+static JavaVM* gVm;
+
+struct TlsState {
+    jobject jobj;
+};
+
+struct IncrementalState {
+    jobject jobj;
+};
+
+static void traceAllPendingPackets(JNIEnv* env, jobject jCtx, PerfettoDsTracerIterator* ctx) {
+    jobjectArray packets =
+            (jobjectArray)env
+                    ->CallObjectMethod(jCtx,
+                                       gTracingContextClassInfo.getAndClearAllPendingTracePackets);
+    if (env->ExceptionOccurred()) {
+        env->ExceptionDescribe();
+        env->ExceptionClear();
+
+        LOG_ALWAYS_FATAL("Failed to call java context finalize method");
+    }
+
+    int packets_count = env->GetArrayLength(packets);
+    for (int i = 0; i < packets_count; i++) {
+        jbyteArray packet_proto_buffer = (jbyteArray)env->GetObjectArrayElement(packets, i);
+
+        jbyte* raw_proto_buffer = env->GetByteArrayElements(packet_proto_buffer, 0);
+        int buffer_size = env->GetArrayLength(packet_proto_buffer);
+
+        struct PerfettoDsRootTracePacket trace_packet;
+        PerfettoDsTracerPacketBegin(ctx, &trace_packet);
+        PerfettoPbMsgAppendBytes(&trace_packet.msg.msg, (const uint8_t*)raw_proto_buffer,
+                                 buffer_size);
+        PerfettoDsTracerPacketEnd(ctx, &trace_packet);
+    }
+}
+
+PerfettoDataSource::PerfettoDataSource(JNIEnv* env, jobject javaDataSource,
+                                       std::string dataSourceName)
+      : dataSourceName(std::move(dataSourceName)),
+        mJavaDataSource(env->NewGlobalRef(javaDataSource)) {}
+
+jobject PerfettoDataSource::newInstance(JNIEnv* env, void* ds_config, size_t ds_config_size,
+                                        PerfettoDsInstanceIndex inst_id) {
+    jbyteArray configArray = env->NewByteArray(ds_config_size);
+
+    void* temp = env->GetPrimitiveArrayCritical((jarray)configArray, 0);
+    memcpy(temp, ds_config, ds_config_size);
+    env->ReleasePrimitiveArrayCritical(configArray, temp, 0);
+
+    jobject instance =
+            env->CallObjectMethod(mJavaDataSource, gPerfettoDataSourceClassInfo.createInstance,
+                                  configArray, inst_id);
+
+    if (env->ExceptionCheck()) {
+        LOGE_EX(env);
+        env->ExceptionClear();
+        LOG_ALWAYS_FATAL("Failed to create new Java Perfetto datasource instance");
+    }
+
+    return instance;
+}
+
+jobject PerfettoDataSource::createTlsStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gCreateTlsStateArgsClassInfo.clazz,
+                                                gCreateTlsStateArgsClassInfo.init, mJavaDataSource,
+                                                inst_id));
+
+    ScopedLocalRef<jobject> tslState(env,
+                                     env->CallObjectMethod(mJavaDataSource,
+                                                           gPerfettoDataSourceClassInfo
+                                                                   .createTlsState,
+                                                           args.get()));
+
+    if (env->ExceptionCheck()) {
+        LOGE_EX(env);
+        env->ExceptionClear();
+        LOG_ALWAYS_FATAL("Failed to create new Java Perfetto incremental state");
+    }
+
+    return env->NewGlobalRef(tslState.get());
+}
+
+jobject PerfettoDataSource::createIncrementalStateGlobalRef(JNIEnv* env,
+                                                            PerfettoDsInstanceIndex inst_id) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gCreateIncrementalStateArgsClassInfo.clazz,
+                                                gCreateIncrementalStateArgsClassInfo.init,
+                                                mJavaDataSource, inst_id));
+
+    ScopedLocalRef<jobject> incrementalState(env,
+                                             env->CallObjectMethod(mJavaDataSource,
+                                                                   gPerfettoDataSourceClassInfo
+                                                                           .createIncrementalState,
+                                                                   args.get()));
+
+    if (env->ExceptionCheck()) {
+        LOGE_EX(env);
+        env->ExceptionClear();
+        LOG_ALWAYS_FATAL("Failed to create Java Perfetto incremental state");
+    }
+
+    return env->NewGlobalRef(incrementalState.get());
+}
+
+void PerfettoDataSource::trace(JNIEnv* env, jobject traceFunction) {
+    PERFETTO_DS_TRACE(dataSource, ctx) {
+        TlsState* tls_state =
+                reinterpret_cast<TlsState*>(PerfettoDsGetCustomTls(&dataSource, &ctx));
+        IncrementalState* incr_state = reinterpret_cast<IncrementalState*>(
+                PerfettoDsGetIncrementalState(&dataSource, &ctx));
+
+        ScopedLocalRef<jobject> jCtx(env,
+                                     env->NewObject(gTracingContextClassInfo.clazz,
+                                                    gTracingContextClassInfo.init, &ctx,
+                                                    tls_state->jobj, incr_state->jobj));
+
+        jclass objclass = env->GetObjectClass(traceFunction);
+        jmethodID method =
+                env->GetMethodID(objclass, "trace", "(Landroid/tracing/perfetto/TracingContext;)V");
+        if (method == 0) {
+            LOG_ALWAYS_FATAL("Failed to get method id");
+        }
+
+        env->ExceptionClear();
+
+        env->CallVoidMethod(traceFunction, method, jCtx.get());
+        if (env->ExceptionOccurred()) {
+            env->ExceptionDescribe();
+            env->ExceptionClear();
+            LOG_ALWAYS_FATAL("Failed to call java trace method");
+        }
+
+        traceAllPendingPackets(env, jCtx.get(), &ctx);
+    }
+}
+
+void PerfettoDataSource::flushAll() {
+    PERFETTO_DS_TRACE(dataSource, ctx) {
+        PerfettoDsTracerFlush(&ctx, nullptr, nullptr);
+    }
+}
+
+PerfettoDataSource::~PerfettoDataSource() {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->DeleteWeakGlobalRef(mJavaDataSource);
+}
+
+jlong nativeCreate(JNIEnv* env, jclass clazz, jobject javaDataSource, jstring name) {
+    const char* nativeString = env->GetStringUTFChars(name, 0);
+    PerfettoDataSource* dataSource = new PerfettoDataSource(env, javaDataSource, nativeString);
+    env->ReleaseStringUTFChars(name, nativeString);
+
+    dataSource->incStrong((void*)nativeCreate);
+
+    return reinterpret_cast<jlong>(dataSource);
+}
+
+void nativeDestroy(void* ptr) {
+    PerfettoDataSource* dataSource = reinterpret_cast<PerfettoDataSource*>(ptr);
+    dataSource->decStrong((void*)nativeCreate);
+}
+
+static jlong nativeGetFinalizer(JNIEnv* /* env */, jclass /* clazz */) {
+    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&nativeDestroy));
+}
+
+void nativeTrace(JNIEnv* env, jclass clazz, jlong dataSourcePtr, jobject traceFunctionInterface) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+
+    datasource->trace(env, traceFunctionInterface);
+}
+
+void nativeFlush(JNIEnv* env, jclass clazz, jobject jCtx, jlong ctxPtr) {
+    auto* ctx = reinterpret_cast<struct PerfettoDsTracerIterator*>(ctxPtr);
+    traceAllPendingPackets(env, jCtx, ctx);
+    PerfettoDsTracerFlush(ctx, nullptr, nullptr);
+}
+
+void nativeFlushAll(JNIEnv* env, jclass clazz, jlong ptr) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(ptr);
+    datasource->flushAll();
+}
+
+void nativeRegisterDataSource(JNIEnv* env, jclass clazz, jlong datasource_ptr,
+                              int buffer_exhausted_policy) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(datasource_ptr);
+
+    struct PerfettoDsParams params = PerfettoDsParamsDefault();
+    params.buffer_exhausted_policy = (PerfettoDsBufferExhaustedPolicy)buffer_exhausted_policy;
+
+    params.user_arg = reinterpret_cast<void*>(datasource.get());
+
+    params.on_setup_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex inst_id,
+                            void* ds_config, size_t ds_config_size, void* user_arg,
+                            struct PerfettoDsOnSetupArgs*) -> void* {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+
+        ScopedLocalRef<jobject> java_data_source_instance(env,
+                                                          datasource->newInstance(env, ds_config,
+                                                                                  ds_config_size,
+                                                                                  inst_id));
+
+        auto* datasource_instance =
+                new PerfettoDataSourceInstance(env, java_data_source_instance.get(), inst_id);
+
+        return static_cast<void*>(datasource_instance);
+    };
+
+    params.on_create_tls_cb = [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id,
+                                 struct PerfettoDsTracerImpl* tracer, void* user_arg) -> void* {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+
+        jobject java_tls_state = datasource->createTlsStateGlobalRef(env, inst_id);
+
+        auto* tls_state = new TlsState(java_tls_state);
+
+        return static_cast<void*>(tls_state);
+    };
+
+    params.on_delete_tls_cb = [](void* ptr) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        TlsState* tls_state = reinterpret_cast<TlsState*>(ptr);
+        env->DeleteGlobalRef(tls_state->jobj);
+        delete tls_state;
+    };
+
+    params.on_create_incr_cb = [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id,
+                                  struct PerfettoDsTracerImpl* tracer, void* user_arg) -> void* {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+        jobject java_incr_state = datasource->createIncrementalStateGlobalRef(env, inst_id);
+
+        auto* incr_state = new IncrementalState(java_incr_state);
+        return static_cast<void*>(incr_state);
+    };
+
+    params.on_delete_incr_cb = [](void* ptr) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        IncrementalState* incr_state = reinterpret_cast<IncrementalState*>(ptr);
+        env->DeleteGlobalRef(incr_state->jobj);
+        delete incr_state;
+    };
+
+    params.on_start_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*, void* inst_ctx,
+                            struct PerfettoDsOnStartArgs*) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+        datasource_instance->onStart(env);
+    };
+
+    params.on_flush_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*, void* inst_ctx,
+                            struct PerfettoDsOnFlushArgs*) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+        datasource_instance->onFlush(env);
+    };
+
+    params.on_stop_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex inst_id, void* user_arg,
+                           void* inst_ctx, struct PerfettoDsOnStopArgs*) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+        datasource_instance->onStop(env);
+    };
+
+    params.on_destroy_cb = [](struct PerfettoDsImpl* ds_impl, void* user_arg,
+                              void* inst_ctx) -> void {
+        auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+        delete datasource_instance;
+    };
+
+    PerfettoDsRegister(&datasource->dataSource, datasource->dataSourceName.c_str(), params);
+}
+
+jobject nativeGetPerfettoInstanceLocked(JNIEnv* env, jclass clazz, jlong dataSourcePtr,
+                                        PerfettoDsInstanceIndex instance_idx) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+    auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(
+            PerfettoDsImplGetInstanceLocked(datasource->dataSource.impl, instance_idx));
+
+    if (datasource_instance == nullptr) {
+        // datasource instance doesn't exist
+        return nullptr;
+    }
+
+    return datasource_instance->GetJavaDataSourceInstance();
+}
+
+void nativeReleasePerfettoInstanceLocked(JNIEnv* env, jclass clazz, jlong dataSourcePtr,
+                                         PerfettoDsInstanceIndex instance_idx) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+    PerfettoDsImplReleaseInstanceLocked(datasource->dataSource.impl, instance_idx);
+}
+
+const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativeCreate", "(Landroid/tracing/perfetto/DataSource;Ljava/lang/String;)J",
+         (void*)nativeCreate},
+        {"nativeTrace", "(JLandroid/tracing/perfetto/TraceFunction;)V", (void*)nativeTrace},
+        {"nativeFlushAll", "(J)V", (void*)nativeFlushAll},
+        {"nativeGetFinalizer", "()J", (void*)nativeGetFinalizer},
+        {"nativeRegisterDataSource", "(JI)V", (void*)nativeRegisterDataSource},
+        {"nativeGetPerfettoInstanceLocked", "(JI)Landroid/tracing/perfetto/DataSourceInstance;",
+         (void*)nativeGetPerfettoInstanceLocked},
+        {"nativeReleasePerfettoInstanceLocked", "(JI)V",
+         (void*)nativeReleasePerfettoInstanceLocked},
+};
+
+const JNINativeMethod gMethodsTracingContext[] = {
+        /* name, signature, funcPtr */
+        {"nativeFlush", "(Landroid/tracing/perfetto/TracingContext;J)V", (void*)nativeFlush},
+};
+
+int register_android_tracing_PerfettoDataSource(JNIEnv* env) {
+    int res = jniRegisterNativeMethods(env, "android/tracing/perfetto/DataSource", gMethods,
+                                       NELEM(gMethods));
+
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+    res = jniRegisterNativeMethods(env, "android/tracing/perfetto/TracingContext",
+                                   gMethodsTracingContext, NELEM(gMethodsTracingContext));
+
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+    if (env->GetJavaVM(&gVm) != JNI_OK) {
+        LOG_ALWAYS_FATAL("Failed to get JavaVM from JNIEnv: %p", env);
+    }
+
+    jclass clazz = env->FindClass("android/tracing/perfetto/DataSource");
+    gPerfettoDataSourceClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gPerfettoDataSourceClassInfo.createInstance =
+            env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createInstance",
+                             "([BI)Landroid/tracing/perfetto/DataSourceInstance;");
+    gPerfettoDataSourceClassInfo.createTlsState =
+            env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createTlsState",
+                             "(Landroid/tracing/perfetto/CreateTlsStateArgs;)Ljava/lang/Object;");
+    gPerfettoDataSourceClassInfo.createIncrementalState =
+            env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createIncrementalState",
+                             "(Landroid/tracing/perfetto/CreateIncrementalStateArgs;)Ljava/lang/"
+                             "Object;");
+
+    clazz = env->FindClass("android/tracing/perfetto/TracingContext");
+    gTracingContextClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gTracingContextClassInfo.init = env->GetMethodID(gTracingContextClassInfo.clazz, "<init>",
+                                                     "(JLjava/lang/Object;Ljava/lang/Object;)V");
+    gTracingContextClassInfo.getAndClearAllPendingTracePackets =
+            env->GetMethodID(gTracingContextClassInfo.clazz, "getAndClearAllPendingTracePackets",
+                             "()[[B");
+
+    clazz = env->FindClass("android/tracing/perfetto/CreateTlsStateArgs");
+    gCreateTlsStateArgsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gCreateTlsStateArgsClassInfo.init =
+            env->GetMethodID(gCreateTlsStateArgsClassInfo.clazz, "<init>",
+                             "(Landroid/tracing/perfetto/DataSource;I)V");
+
+    clazz = env->FindClass("android/tracing/perfetto/CreateIncrementalStateArgs");
+    gCreateIncrementalStateArgsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gCreateIncrementalStateArgsClassInfo.init =
+            env->GetMethodID(gCreateIncrementalStateArgsClassInfo.clazz, "<init>",
+                             "(Landroid/tracing/perfetto/DataSource;I)V");
+
+    return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSource.h b/core/jni/android_tracing_PerfettoDataSource.h
new file mode 100644
index 0000000..4ddf1d8
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSource.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "android_tracing_PerfettoDataSourceInstance.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+class PerfettoDataSource : public virtual RefBase {
+public:
+    const std::string dataSourceName;
+    struct PerfettoDs dataSource = PERFETTO_DS_INIT();
+
+    PerfettoDataSource(JNIEnv* env, jobject java_data_source, std::string data_source_name);
+    ~PerfettoDataSource();
+
+    jobject newInstance(JNIEnv* env, void* ds_config, size_t ds_config_size,
+                        PerfettoDsInstanceIndex inst_id);
+
+    jobject createTlsStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id);
+    jobject createIncrementalStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id);
+    void trace(JNIEnv* env, jobject trace_function);
+    void flushAll();
+
+private:
+    jobject mJavaDataSource;
+    std::map<PerfettoDsInstanceIndex, PerfettoDataSourceInstance*> mInstances;
+};
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.cpp b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
new file mode 100644
index 0000000..e659bf1
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include "android_tracing_PerfettoDataSourceInstance.h"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gStartCallbackArgumentsClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gFlushCallbackArgumentsClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gStopCallbackArgumentsClassInfo;
+
+static JavaVM* gVm;
+
+void callJavaMethodWithArgsObject(JNIEnv* env, jobject classRef, jmethodID method, jobject args) {
+    ScopedLocalRef<jobject> localClassRef(env, env->NewLocalRef(classRef));
+
+    if (localClassRef == nullptr) {
+        ALOGE("Weak reference went out of scope");
+        return;
+    }
+
+    env->CallVoidMethod(localClassRef.get(), method, args);
+
+    if (env->ExceptionCheck()) {
+        env->ExceptionDescribe();
+        LOGE_EX(env);
+        env->ExceptionClear();
+    }
+}
+
+PerfettoDataSourceInstance::PerfettoDataSourceInstance(JNIEnv* env, jobject javaDataSourceInstance,
+                                                       PerfettoDsInstanceIndex inst_idx)
+      : inst_idx(inst_idx), mJavaDataSourceInstance(env->NewGlobalRef(javaDataSourceInstance)) {}
+
+PerfettoDataSourceInstance::~PerfettoDataSourceInstance() {
+    JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+    env->DeleteGlobalRef(mJavaDataSourceInstance);
+}
+
+void PerfettoDataSourceInstance::onStart(JNIEnv* env) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gStartCallbackArgumentsClassInfo.clazz,
+                                                gStartCallbackArgumentsClassInfo.init));
+    jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+    jmethodID mid = env->GetMethodID(cls, "onStart",
+                                     "(Landroid/tracing/perfetto/StartCallbackArguments;)V");
+
+    callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+void PerfettoDataSourceInstance::onFlush(JNIEnv* env) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gFlushCallbackArgumentsClassInfo.clazz,
+                                                gFlushCallbackArgumentsClassInfo.init));
+    jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+    jmethodID mid = env->GetMethodID(cls, "onFlush",
+                                     "(Landroid/tracing/perfetto/FlushCallbackArguments;)V");
+
+    callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+void PerfettoDataSourceInstance::onStop(JNIEnv* env) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gStopCallbackArgumentsClassInfo.clazz,
+                                                gStopCallbackArgumentsClassInfo.init));
+    jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+    jmethodID mid =
+            env->GetMethodID(cls, "onStop", "(Landroid/tracing/perfetto/StopCallbackArguments;)V");
+
+    callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+int register_android_tracing_PerfettoDataSourceInstance(JNIEnv* env) {
+    if (env->GetJavaVM(&gVm) != JNI_OK) {
+        LOG_ALWAYS_FATAL("Failed to get JavaVM from JNIEnv: %p", env);
+    }
+
+    jclass clazz = env->FindClass("android/tracing/perfetto/StartCallbackArguments");
+    gStartCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gStartCallbackArgumentsClassInfo.init =
+            env->GetMethodID(gStartCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+    clazz = env->FindClass("android/tracing/perfetto/FlushCallbackArguments");
+    gFlushCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gFlushCallbackArgumentsClassInfo.init =
+            env->GetMethodID(gFlushCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+    clazz = env->FindClass("android/tracing/perfetto/StopCallbackArguments");
+    gStopCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gStopCallbackArgumentsClassInfo.init =
+            env->GetMethodID(gStopCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+    return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.h b/core/jni/android_tracing_PerfettoDataSourceInstance.h
new file mode 100644
index 0000000..d577655
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+class PerfettoDataSourceInstance {
+public:
+    PerfettoDataSourceInstance(JNIEnv* env, jobject javaDataSourceInstance,
+                               PerfettoDsInstanceIndex inst_idx);
+    ~PerfettoDataSourceInstance();
+
+    void onStart(JNIEnv* env);
+    void onFlush(JNIEnv* env);
+    void onStop(JNIEnv* env);
+
+    jobject GetJavaDataSourceInstance() {
+        return mJavaDataSourceInstance;
+    }
+
+    PerfettoDsInstanceIndex getIndex() {
+        return inst_idx;
+    }
+
+private:
+    PerfettoDsInstanceIndex inst_idx;
+    jobject mJavaDataSourceInstance;
+};
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoProducer.cpp b/core/jni/android_tracing_PerfettoProducer.cpp
new file mode 100644
index 0000000..ce72f58
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoProducer.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "android_tracing_PerfettoDataSource.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+void perfettoProducerInit(JNIEnv* env, jclass clazz, int backends) {
+    struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
+    args.backends = (PerfettoBackendTypes)backends;
+    PerfettoProducerInit(args);
+}
+
+const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativePerfettoProducerInit", "(I)V", (void*)perfettoProducerInit},
+};
+
+int register_android_tracing_PerfettoProducer(JNIEnv* env) {
+    int res = jniRegisterNativeMethods(env, "android/tracing/perfetto/Producer", gMethods,
+                                       NELEM(gMethods));
+
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+    return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto
index a2978be..ad36b1c 100644
--- a/core/proto/android/service/notification.proto
+++ b/core/proto/android/service/notification.proto
@@ -360,7 +360,7 @@
 
     optional ConversationType allow_conversations_from = 19;
 
-    optional ChannelType allow_channels = 20;
+    optional ChannelPolicy allow_channels = 20;
 }
 
 // Enum identifying the type of rule that changed; values set to match ones used in the
@@ -373,8 +373,8 @@
 
 // Enum used in DNDPolicyProto to indicate the type of channels permitted to
 // break through DND. Mirrors values in ZenPolicy.
-enum ChannelType {
-    CHANNEL_TYPE_UNSET = 0;
-    CHANNEL_TYPE_PRIORITY = 1;
-    CHANNEL_TYPE_NONE = 2;
+enum ChannelPolicy {
+    CHANNEL_POLICY_UNSET = 0;
+    CHANNEL_POLICY_PRIORITY = 1;
+    CHANNEL_POLICY_NONE = 2;
 }
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 6019524..8fae6db 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -506,12 +506,6 @@
          receivers, and providers; it can not be used with activities. -->
     <attr name="singleUser" format="boolean" />
 
-    <!-- If set to true, only a single instance of this component will
-    run and be available for the SYSTEM user. Non SYSTEM users will not be
-    allowed to access the component if this flag is enabled.
-    This flag can be used with services, receivers, providers and activities. -->
-    <attr name="systemUserOnly" format="boolean" />
-
     <!-- Specify a specific process that the associated code is to run in.
          Use with the application tag (to supply a default process for all
          application components), or with the activity, receiver, service,
@@ -2865,7 +2859,6 @@
              Context.createAttributionContext() using the first attribution tag
              contained here. -->
         <attr name="attributionTags" />
-        <attr name="systemUserOnly" format="boolean" />
     </declare-styleable>
 
     <!-- Attributes that can be supplied in an AndroidManifest.xml
@@ -3024,7 +3017,6 @@
              ignored when the process is bound into a shared isolated process by a client.
         -->
         <attr name="allowSharedIsolatedProcess" format="boolean" />
-        <attr name="systemUserOnly" format="boolean" />
     </declare-styleable>
 
     <!-- @hide The <code>apex-system-service</code> tag declares an apex system service
@@ -3152,7 +3144,7 @@
         <attr name="uiOptions" />
         <attr name="parentActivityName" />
         <attr name="singleUser" />
-        <!-- This broadcast receiver or activity will only receive broadcasts for the
+        <!-- @hide This broadcast receiver or activity will only receive broadcasts for the
              system user-->
         <attr name="systemUserOnly" format="boolean" />
         <attr name="persistableMode" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5be29a6..28adccd 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -57,6 +57,7 @@
         <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_ethernet</xliff:g></item>
+        <item><xliff:g id="id">@string/status_bar_oem_satellite</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_mobile</xliff:g></item>
@@ -102,6 +103,7 @@
     <string translatable="false" name="status_bar_call_strength">call_strength</string>
     <string translatable="false" name="status_bar_sensors_off">sensors_off</string>
     <string translatable="false" name="status_bar_screen_record">screen_record</string>
+    <string translatable="false" name="status_bar_oem_satellite">satellite</string>
 
     <!-- Flag indicating whether the surface flinger has limited
          alpha compositing functionality in hardware.  If set, the window
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 7b5c49c..53b473e 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -119,8 +119,6 @@
     <public name="optional"/>
     <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") -->
     <public name="adServiceTypes" />
-    <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") -->
-    <public name="systemUserOnly"/>
   </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 d12ef2b..ef12d8f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3198,6 +3198,7 @@
   <java-symbol type="string" name="status_bar_camera" />
   <java-symbol type="string" name="status_bar_sensors_off" />
   <java-symbol type="string" name="status_bar_screen_record" />
+  <java-symbol type="string" name="status_bar_oem_satellite" />
 
   <!-- Locale picker -->
   <java-symbol type="id" name="locale_search_menu" />
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index c058174..e18de2e 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -30,6 +30,8 @@
 
 android_test {
     name: "FrameworksCoreTests",
+    // FrameworksCoreTestsRavenwood references the .aapt.srcjar
+    use_resource_processor: false,
 
     srcs: [
         "src/**/*.java",
@@ -83,6 +85,10 @@
         "com.android.text.flags-aconfig-java",
         "flag-junit",
         "ravenwood-junit",
+        "perfetto_trace_java_protos",
+        "flickerlib-parsers",
+        "flickerlib-trace_processor_shell",
+        "mockito-target-extended-minus-junit4",
     ],
 
     libs: [
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
new file mode 100644
index 0000000..bd2f36f
--- /dev/null
+++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import static android.internal.perfetto.protos.PerfettoTrace.TestEvent.PAYLOAD;
+import static android.internal.perfetto.protos.PerfettoTrace.TestEvent.TestPayload.SINGLE_INT;
+import static android.internal.perfetto.protos.PerfettoTrace.TracePacket.FOR_TESTING;
+
+import static java.io.File.createTempFile;
+import static java.nio.file.Files.createTempDirectory;
+
+import android.internal.perfetto.protos.PerfettoTrace;
+import android.tools.common.ScenarioBuilder;
+import android.tools.common.Tag;
+import android.tools.common.io.TraceType;
+import android.tools.device.traces.TraceConfig;
+import android.tools.device.traces.TraceConfigs;
+import android.tools.device.traces.io.ResultReader;
+import android.tools.device.traces.io.ResultWriter;
+import android.tools.device.traces.monitors.PerfettoTraceMonitor;
+import android.tools.device.traces.monitors.TraceMonitor;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Truth;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import perfetto.protos.PerfettoConfig;
+import perfetto.protos.TracePacketOuterClass;
+
+@RunWith(AndroidJUnit4.class)
+public class DataSourceTest {
+    private final File mTracingDirectory = createTempDirectory("temp").toFile();
+
+    private final ResultWriter mWriter = new ResultWriter()
+            .forScenario(new ScenarioBuilder()
+                    .forClass(createTempFile("temp", "").getName()).build())
+            .withOutputDir(mTracingDirectory)
+            .setRunComplete();
+
+    private final TraceConfigs mTraceConfig = new TraceConfigs(
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false)
+    );
+
+    private static TestDataSource sTestDataSource;
+
+    private static TestDataSource.DataSourceInstanceProvider sInstanceProvider;
+    private static TestDataSource.TlsStateProvider sTlsStateProvider;
+    private static TestDataSource.IncrementalStateProvider sIncrementalStateProvider;
+
+    public DataSourceTest() throws IOException {}
+
+    @BeforeClass
+    public static void beforeAll() {
+        Producer.init(InitArguments.DEFAULTS);
+        setupProviders();
+        sTestDataSource = new TestDataSource(
+                (ds, idx, configStream) -> sInstanceProvider.provide(ds, idx, configStream),
+                args -> sTlsStateProvider.provide(args),
+                args -> sIncrementalStateProvider.provide(args));
+        sTestDataSource.register(DataSourceParams.DEFAULTS);
+    }
+
+    private static void setupProviders() {
+        sInstanceProvider = (ds, idx, configStream) ->
+                new TestDataSource.TestDataSourceInstance(ds, idx);
+        sTlsStateProvider = args -> new TestDataSource.TestTlsState();
+        sIncrementalStateProvider = args -> new TestDataSource.TestIncrementalState();
+    }
+
+    @Before
+    public void setup() {
+        setupProviders();
+    }
+
+    @Test
+    public void canTraceData() throws InvalidProtocolBufferException {
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+
+            sTestDataSource.trace((ctx) -> {
+                final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+                long forTestingToken = protoOutputStream.start(FOR_TESTING);
+                long payloadToken = protoOutputStream.start(PAYLOAD);
+                protoOutputStream.write(SINGLE_INT, 10);
+                protoOutputStream.end(payloadToken);
+                protoOutputStream.end(forTestingToken);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+        assert rawProtoFromFile != null;
+        final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+                .parseFrom(rawProtoFromFile);
+
+        Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+        final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+                .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+        final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+                .filter(it -> it.getForTesting().getPayload().getSingleInt() == 10).toList();
+        Truth.assertThat(matchingPackets).hasSize(1);
+    }
+
+    @Test
+    public void canUseTlsStateForCustomState() {
+        final int expectedStateTestValue = 10;
+        final AtomicInteger actualStateTestValue = new AtomicInteger();
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+
+            sTestDataSource.trace((ctx) -> {
+                TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                state.testStateValue = expectedStateTestValue;
+            });
+
+            sTestDataSource.trace((ctx) -> {
+                TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                actualStateTestValue.set(state.testStateValue);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        Truth.assertThat(actualStateTestValue.get()).isEqualTo(expectedStateTestValue);
+    }
+
+    @Test
+    public void eachInstanceHasOwnTlsState() {
+        final int[] expectedStateTestValues = new int[] { 1, 2 };
+        final int[] actualStateTestValues = new int[] { 0, 0 };
+
+        final TraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+        final TraceMonitor traceMonitor2 = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor1.start();
+            try {
+                traceMonitor2.start();
+
+                AtomicInteger index = new AtomicInteger(0);
+                sTestDataSource.trace((ctx) -> {
+                    TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                    state.testStateValue = expectedStateTestValues[index.getAndIncrement()];
+                });
+
+                index.set(0);
+                sTestDataSource.trace((ctx) -> {
+                    TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                    actualStateTestValues[index.getAndIncrement()] = state.testStateValue;
+                });
+            } finally {
+                traceMonitor1.stop(mWriter);
+            }
+        } finally {
+            traceMonitor2.stop(mWriter);
+        }
+
+        Truth.assertThat(actualStateTestValues[0]).isEqualTo(expectedStateTestValues[0]);
+        Truth.assertThat(actualStateTestValues[1]).isEqualTo(expectedStateTestValues[1]);
+    }
+
+    @Test
+    public void eachThreadHasOwnTlsState() throws InterruptedException {
+        final int thread1ExpectedStateValue = 1;
+        final int thread2ExpectedStateValue = 2;
+
+        final AtomicInteger thread1ActualStateValue = new AtomicInteger();
+        final AtomicInteger thread2ActualStateValue = new AtomicInteger();
+
+        final CountDownLatch setUpLatch = new CountDownLatch(2);
+        final CountDownLatch setStateLatch = new CountDownLatch(2);
+        final CountDownLatch setOutStateLatch = new CountDownLatch(2);
+
+        final RunnableCreator createTask = (stateValue, stateOut) -> () -> {
+            Producer.init(InitArguments.DEFAULTS);
+
+            setUpLatch.countDown();
+
+            try {
+                setUpLatch.await(3, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            sTestDataSource.trace((ctx) -> {
+                TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                state.testStateValue = stateValue;
+                setStateLatch.countDown();
+            });
+
+            try {
+                setStateLatch.await(3, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            sTestDataSource.trace((ctx) -> {
+                stateOut.set(ctx.getCustomTlsState().testStateValue);
+                setOutStateLatch.countDown();
+            });
+        };
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+
+            new Thread(
+                    createTask.create(thread1ExpectedStateValue, thread1ActualStateValue)).start();
+            new Thread(
+                    createTask.create(thread2ExpectedStateValue, thread2ActualStateValue)).start();
+
+            setOutStateLatch.await(3, TimeUnit.SECONDS);
+
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        Truth.assertThat(thread1ActualStateValue.get()).isEqualTo(thread1ExpectedStateValue);
+        Truth.assertThat(thread2ActualStateValue.get()).isEqualTo(thread2ExpectedStateValue);
+    }
+
+    @Test
+    public void incrementalStateIsReset() throws InterruptedException {
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build())
+                .setIncrementalTimeout(10)
+                .build();
+
+        final AtomicInteger testStateValue = new AtomicInteger();
+        try {
+            traceMonitor.start();
+
+            sTestDataSource.trace(ctx -> ctx.getIncrementalState().testStateValue = 1);
+
+            // Timeout to make sure the incremental state is cleared.
+            Thread.sleep(1000);
+
+            sTestDataSource.trace(ctx ->
+                    testStateValue.set(ctx.getIncrementalState().testStateValue));
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        Truth.assertThat(testStateValue.get()).isNotEqualTo(1);
+    }
+
+    @Test
+    public void getInstanceConfigOnCreateInstance() throws IOException {
+        final int expectedDummyIntValue = 10;
+        AtomicReference<ProtoInputStream> configStream = new AtomicReference<>();
+        sInstanceProvider = (ds, idx, config) -> {
+            configStream.set(config);
+            return new TestDataSource.TestDataSourceInstance(ds, idx);
+        };
+
+        final TraceMonitor monitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name)
+                        .setForTesting(PerfettoConfig.TestConfig.newBuilder().setDummyFields(
+                                PerfettoConfig.TestConfig.DummyFields.newBuilder()
+                                        .setFieldInt32(expectedDummyIntValue)
+                                        .build())
+                                .build())
+                        .build())
+                .build();
+
+        try {
+            monitor.start();
+        } finally {
+            monitor.stop(mWriter);
+        }
+
+        int configDummyIntValue = 0;
+        while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            if (configStream.get().getFieldNumber()
+                    == (int) PerfettoTrace.DataSourceConfig.FOR_TESTING) {
+                final long forTestingToken = configStream.get()
+                        .start(PerfettoTrace.DataSourceConfig.FOR_TESTING);
+                while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    if (configStream.get().getFieldNumber()
+                            == (int) PerfettoTrace.TestConfig.DUMMY_FIELDS) {
+                        final long dummyFieldsToken = configStream.get()
+                                .start(PerfettoTrace.TestConfig.DUMMY_FIELDS);
+                        while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                            if (configStream.get().getFieldNumber()
+                                    == (int) PerfettoTrace.TestConfig.DummyFields.FIELD_INT32) {
+                                int val = configStream.get().readInt(
+                                        PerfettoTrace.TestConfig.DummyFields.FIELD_INT32);
+                                if (val != 0) {
+                                    configDummyIntValue = val;
+                                    break;
+                                }
+                            }
+                        }
+                        configStream.get().end(dummyFieldsToken);
+                        break;
+                    }
+                }
+                configStream.get().end(forTestingToken);
+                break;
+            }
+        }
+
+        Truth.assertThat(configDummyIntValue).isEqualTo(expectedDummyIntValue);
+    }
+
+    @Test
+    public void multipleTraceInstances() throws IOException, InterruptedException {
+        final int instanceCount = 3;
+
+        final List<TraceMonitor> monitors = new ArrayList<>();
+        final List<ResultWriter> writers = new ArrayList<>();
+
+        for (int i = 0; i < instanceCount; i++) {
+            final ResultWriter writer = new ResultWriter()
+                    .forScenario(new ScenarioBuilder()
+                            .forClass(createTempFile("temp", "").getName()).build())
+                    .withOutputDir(mTracingDirectory)
+                    .setRunComplete();
+            writers.add(writer);
+        }
+
+        // Start at 1 because 0 is considered null value so payload will be ignored in that case
+        TestDataSource.TestTlsState.lastIndex = 1;
+
+        final AtomicInteger traceCallCount = new AtomicInteger();
+        final CountDownLatch latch = new CountDownLatch(instanceCount);
+
+        try {
+            // Start instances
+            for (int i = 0; i < instanceCount; i++) {
+                final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                        .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                                .setName(sTestDataSource.name).build()).build();
+                monitors.add(traceMonitor);
+                traceMonitor.start();
+            }
+
+            // Trace the stateIndex of the tracing instance.
+            sTestDataSource.trace(ctx -> {
+                final int testIntValue = ctx.getCustomTlsState().stateIndex;
+                traceCallCount.incrementAndGet();
+
+                final ProtoOutputStream os = ctx.newTracePacket();
+                long forTestingToken = os.start(FOR_TESTING);
+                long payloadToken = os.start(PAYLOAD);
+                os.write(SINGLE_INT, testIntValue);
+                os.end(payloadToken);
+                os.end(forTestingToken);
+
+                latch.countDown();
+            });
+        } finally {
+            // Stop instances
+            for (int i = 0; i < instanceCount; i++) {
+                final TraceMonitor monitor = monitors.get(i);
+                final ResultWriter writer = writers.get(i);
+                monitor.stop(writer);
+            }
+        }
+
+        latch.await(3, TimeUnit.SECONDS);
+        Truth.assertThat(traceCallCount.get()).isEqualTo(instanceCount);
+
+        for (int i = 0; i < instanceCount; i++) {
+            final int expectedTracedValue = i + 1;
+            final ResultWriter writer = writers.get(i);
+            final ResultReader reader = new ResultReader(writer.write(), mTraceConfig);
+            final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+            assert rawProtoFromFile != null;
+            final perfetto.protos.TraceOuterClass.Trace trace =
+                    perfetto.protos.TraceOuterClass.Trace.parseFrom(rawProtoFromFile);
+
+            Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+            final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+                    .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+            Truth.assertWithMessage("One packet has for testing data")
+                    .that(tracePackets).hasSize(1);
+
+            final List<TracePacketOuterClass.TracePacket> matchingPackets =
+                    tracePackets.stream()
+                            .filter(it -> it.getForTesting().getPayload()
+                                    .getSingleInt() == expectedTracedValue).toList();
+            Truth.assertWithMessage(
+                            "One packet has testing data with a payload with the expected value")
+                    .that(matchingPackets).hasSize(1);
+        }
+    }
+
+    @Test
+    public void onStartCallbackTriggered() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+        sInstanceProvider = (ds, idx, config) -> new TestDataSource.TestDataSourceInstance(
+                        ds,
+                        idx,
+                        (args) -> {
+                            callbackCalled.set(true);
+                            latch.countDown();
+                        },
+                        (args) -> {},
+                        (args) -> {}
+        );
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        Truth.assertThat(callbackCalled.get()).isFalse();
+        try {
+            traceMonitor.start();
+            latch.await(3, TimeUnit.SECONDS);
+            Truth.assertThat(callbackCalled.get()).isTrue();
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+    }
+
+    @Test
+    public void onFlushCallbackTriggered() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+        sInstanceProvider = (ds, idx, config) ->
+                new TestDataSource.TestDataSourceInstance(
+                        ds,
+                        idx,
+                        (args) -> {},
+                        (args) -> {
+                            callbackCalled.set(true);
+                            latch.countDown();
+                        },
+                        (args) -> {}
+                );
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+            Truth.assertThat(callbackCalled.get()).isFalse();
+            sTestDataSource.trace((ctx) -> {
+                final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+                long forTestingToken = protoOutputStream.start(FOR_TESTING);
+                long payloadToken = protoOutputStream.start(PAYLOAD);
+                protoOutputStream.write(SINGLE_INT, 10);
+                protoOutputStream.end(payloadToken);
+                protoOutputStream.end(forTestingToken);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        latch.await(3, TimeUnit.SECONDS);
+        Truth.assertThat(callbackCalled.get()).isTrue();
+    }
+
+    @Test
+    public void onStopCallbackTriggered() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+        sInstanceProvider = (ds, idx, config) ->
+                new TestDataSource.TestDataSourceInstance(
+                        ds,
+                        idx,
+                        (args) -> {},
+                        (args) -> {},
+                        (args) -> {
+                            callbackCalled.set(true);
+                            latch.countDown();
+                        }
+                );
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+            Truth.assertThat(callbackCalled.get()).isFalse();
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        latch.await(3, TimeUnit.SECONDS);
+        Truth.assertThat(callbackCalled.get()).isTrue();
+    }
+
+    @Test
+    public void canUseDataSourceInstanceToCreateTlsState() throws InvalidProtocolBufferException {
+        final Object testObject = new Object();
+
+        sInstanceProvider = (ds, idx, configStream) -> {
+            final TestDataSource.TestDataSourceInstance dsInstance =
+                    new TestDataSource.TestDataSourceInstance(ds, idx);
+            dsInstance.testObject = testObject;
+            return dsInstance;
+        };
+
+        sTlsStateProvider = args -> {
+            final TestDataSource.TestTlsState tlsState = new TestDataSource.TestTlsState();
+
+            try (TestDataSource.TestDataSourceInstance dataSourceInstance =
+                         args.getDataSourceInstanceLocked()) {
+                if (dataSourceInstance != null) {
+                    tlsState.testStateValue = dataSourceInstance.testObject.hashCode();
+                }
+            }
+
+            return tlsState;
+        };
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+            sTestDataSource.trace((ctx) -> {
+                final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+                long forTestingToken = protoOutputStream.start(FOR_TESTING);
+                long payloadToken = protoOutputStream.start(PAYLOAD);
+                protoOutputStream.write(SINGLE_INT, ctx.getCustomTlsState().testStateValue);
+                protoOutputStream.end(payloadToken);
+                protoOutputStream.end(forTestingToken);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+        assert rawProtoFromFile != null;
+        final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+                .parseFrom(rawProtoFromFile);
+
+        Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+        final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+                .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+        final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+                .filter(it -> it.getForTesting().getPayload().getSingleInt()
+                        == testObject.hashCode()).toList();
+        Truth.assertThat(matchingPackets).hasSize(1);
+    }
+
+    @Test
+    public void canUseDataSourceInstanceToCreateIncrementalState()
+            throws InvalidProtocolBufferException {
+        final Object testObject = new Object();
+
+        sInstanceProvider = (ds, idx, configStream) -> {
+            final TestDataSource.TestDataSourceInstance dsInstance =
+                    new TestDataSource.TestDataSourceInstance(ds, idx);
+            dsInstance.testObject = testObject;
+            return dsInstance;
+        };
+
+        sIncrementalStateProvider = args -> {
+            final TestDataSource.TestIncrementalState incrementalState =
+                    new TestDataSource.TestIncrementalState();
+
+            try (TestDataSource.TestDataSourceInstance dataSourceInstance =
+                    args.getDataSourceInstanceLocked()) {
+                if (dataSourceInstance != null) {
+                    incrementalState.testStateValue = dataSourceInstance.testObject.hashCode();
+                }
+            }
+
+            return incrementalState;
+        };
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+            sTestDataSource.trace((ctx) -> {
+                final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+                long forTestingToken = protoOutputStream.start(FOR_TESTING);
+                long payloadToken = protoOutputStream.start(PAYLOAD);
+                protoOutputStream.write(SINGLE_INT, ctx.getIncrementalState().testStateValue);
+                protoOutputStream.end(payloadToken);
+                protoOutputStream.end(forTestingToken);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+        assert rawProtoFromFile != null;
+        final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+                .parseFrom(rawProtoFromFile);
+
+        Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+        final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+                .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+        final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+                .filter(it -> it.getForTesting().getPayload().getSingleInt()
+                        == testObject.hashCode()).toList();
+        Truth.assertThat(matchingPackets).hasSize(1);
+    }
+
+    interface RunnableCreator {
+        Runnable create(int state, AtomicInteger stateOut);
+    }
+}
diff --git a/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java b/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java
new file mode 100644
index 0000000..d78f78b
--- /dev/null
+++ b/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.util.proto.ProtoInputStream;
+
+import java.util.UUID;
+import java.util.function.Consumer;
+
+public class TestDataSource extends DataSource<TestDataSource.TestDataSourceInstance,
+        TestDataSource.TestTlsState, TestDataSource.TestIncrementalState> {
+    private final DataSourceInstanceProvider mDataSourceInstanceProvider;
+    private final TlsStateProvider mTlsStateProvider;
+    private final IncrementalStateProvider mIncrementalStateProvider;
+
+    interface DataSourceInstanceProvider {
+        TestDataSourceInstance provide(
+                TestDataSource dataSource, int instanceIndex, ProtoInputStream configStream);
+    }
+
+    interface TlsStateProvider {
+        TestTlsState provide(CreateTlsStateArgs<TestDataSourceInstance> args);
+    }
+
+    interface IncrementalStateProvider {
+        TestIncrementalState provide(CreateIncrementalStateArgs<TestDataSourceInstance> args);
+    }
+
+    public TestDataSource() {
+        this((ds, idx, config) -> new TestDataSourceInstance(ds, idx),
+                args -> new TestTlsState(), args -> new TestIncrementalState());
+    }
+
+    public TestDataSource(
+            DataSourceInstanceProvider dataSourceInstanceProvider,
+            TlsStateProvider tlsStateProvider,
+            IncrementalStateProvider incrementalStateProvider
+    ) {
+        super("android.tracing.perfetto.TestDataSource#" + UUID.randomUUID().toString());
+        this.mDataSourceInstanceProvider = dataSourceInstanceProvider;
+        this.mTlsStateProvider = tlsStateProvider;
+        this.mIncrementalStateProvider = incrementalStateProvider;
+    }
+
+    @Override
+    public TestDataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) {
+        return mDataSourceInstanceProvider.provide(this, instanceIndex, configStream);
+    }
+
+    @Override
+    public TestTlsState createTlsState(CreateTlsStateArgs args) {
+        return mTlsStateProvider.provide(args);
+    }
+
+    @Override
+    public TestIncrementalState createIncrementalState(CreateIncrementalStateArgs args) {
+        return mIncrementalStateProvider.provide(args);
+    }
+
+    public static class TestTlsState {
+        public int testStateValue;
+        public int stateIndex = lastIndex++;
+
+        public static int lastIndex = 0;
+    }
+
+    public static class TestIncrementalState {
+        public int testStateValue;
+    }
+
+    public static class TestDataSourceInstance extends DataSourceInstance {
+        public Object testObject;
+        Consumer<StartCallbackArguments> mStartCallback;
+        Consumer<FlushCallbackArguments> mFlushCallback;
+        Consumer<StopCallbackArguments> mStopCallback;
+
+        public TestDataSourceInstance(DataSource dataSource, int instanceIndex) {
+            this(dataSource, instanceIndex, args -> {}, args -> {}, args -> {});
+        }
+
+        public TestDataSourceInstance(
+                DataSource dataSource,
+                int instanceIndex,
+                Consumer<StartCallbackArguments> startCallback,
+                Consumer<FlushCallbackArguments> flushCallback,
+                Consumer<StopCallbackArguments> stopCallback) {
+            super(dataSource, instanceIndex);
+            this.mStartCallback = startCallback;
+            this.mFlushCallback = flushCallback;
+            this.mStopCallback = stopCallback;
+        }
+
+        @Override
+        public void onStart(StartCallbackArguments args) {
+            this.mStartCallback.accept(args);
+        }
+
+        @Override
+        public void onFlush(FlushCallbackArguments args) {
+            this.mFlushCallback.accept(args);
+        }
+
+        @Override
+        public void onStop(StopCallbackArguments args) {
+            this.mStopCallback.accept(args);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java b/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java
index 84c93c2..80061a5 100644
--- a/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java
@@ -18,7 +18,6 @@
 
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
 
 import android.os.FileUtils;
 
@@ -73,8 +72,7 @@
     @Test
     public void testReadNonexistentFile() throws Exception {
         mStoragedUidIoStatsReader.readAbsolute(mCallback);
-        verifyZeroInteractions(mCallback);
-
+        verifyNoMoreInteractions(mCallback);
     }
 
     /**
diff --git a/data/etc/com.android.settings.xml b/data/etc/com.android.settings.xml
index dcc9686..fbe1b8e 100644
--- a/data/etc/com.android.settings.xml
+++ b/data/etc/com.android.settings.xml
@@ -48,6 +48,7 @@
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
         <permission name="android.permission.READ_SEARCH_INDEXABLES"/>
         <permission name="android.permission.REBOOT"/>
+        <permission name="android.permission.RECOVERY"/>
         <permission name="android.permission.STATUS_BAR"/>
         <permission name="android.permission.SUGGEST_MANUAL_TIME_AND_ZONE"/>
         <permission name="android.permission.TETHER_PRIVILEGED"/>
diff --git a/framework-jarjar-rules.txt b/framework-jarjar-rules.txt
index 03b268d..6339a87 100644
--- a/framework-jarjar-rules.txt
+++ b/framework-jarjar-rules.txt
@@ -8,3 +8,6 @@
 
 # for modules-utils-build dependency
 rule com.android.modules.utils.build.** android.internal.modules.utils.build.@1
+
+# For Perfetto proto dependencies
+rule perfetto.protos.** android.internal.perfetto.protos.@1
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index c5a2f98..f6ba103 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -65,8 +65,6 @@
     private long mNativeShader;
     private long mNativeColorFilter;
 
-    private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric");
-
     // Use a Holder to allow static initialization of Paint in the boot image.
     private static class NoImagePreloadHolder {
         public static final NativeAllocationRegistry sRegistry =
@@ -3393,13 +3391,8 @@
             return 0.0f;
         }
 
-        if (sIsRobolectric) {
-            return nGetRunCharacterAdvance(mNativePaint, text, start, end,
-                    contextStart, contextEnd, isRtl, offset, advances, advancesIndex, drawBounds);
-        } else {
-            return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
-                    isRtl, offset, advances, advancesIndex, drawBounds, runInfo);
-        }
+        return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
+                isRtl, offset, advances, advancesIndex, drawBounds, runInfo);
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 066f38b..83d555c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -822,11 +822,6 @@
         // Checks if container should be updated before apply new parentInfo.
         final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo);
         taskContainer.updateTaskFragmentParentInfo(parentInfo);
-        if (!taskContainer.isVisible()) {
-            // Don't update containers if the task is not visible. We only update containers when
-            // parentInfo#isVisibleRequested is true.
-            return;
-        }
 
         // If the last direct activity of the host task is dismissed and the overlay container is
         // the only taskFragment, the overlay container should also be dismissed.
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index bc92101..4e7b760 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -475,8 +475,10 @@
     @Test
     public void testOnTaskFragmentParentInfoChanged_positionOnlyChange_earlyReturn() {
         final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test");
-
         final TaskContainer taskContainer = overlayContainer.getTaskContainer();
+
+        assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer);
+
         spyOn(taskContainer);
         final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties();
         final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(
@@ -495,6 +497,30 @@
                 .that(taskContainer.getOverlayContainer()).isNull();
     }
 
+    @Test
+    public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissOverlayContainer() {
+        final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test");
+        final TaskContainer taskContainer = overlayContainer.getTaskContainer();
+
+        assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer);
+
+        spyOn(taskContainer);
+        final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties();
+        final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(
+                new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(),
+                false /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
+
+        mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo);
+
+        // The parent info must be applied to the task container
+        verify(taskContainer).updateTaskFragmentParentInfo(parentInfo);
+        verify(mSplitController, never()).updateContainer(any(), any());
+
+        assertWithMessage("The overlay container must still be dismissed even if "
+                + "#updateContainer is not called")
+                .that(taskContainer.getOverlayContainer()).isNull();
+    }
+
     /**
      * A simplified version of {@link SplitController.ActivityStartMonitor
      * #createOrUpdateOverlayTaskFragmentIfNeeded}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 896bcaf..e23e15f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1249,6 +1249,7 @@
         }
 
         String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
+        Log.v(TAG, "showOrHideAppBubble, with key: " + appBubbleKey);
         PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier());
         if (!isResizableActivity(intent, packageManager, appBubbleKey)) return;
 
@@ -1258,18 +1259,22 @@
             if (isStackExpanded()) {
                 if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) {
                     // App bubble is expanded, lets collapse
+                    Log.v(TAG, "  showOrHideAppBubble, selected bubble is app bubble, collapsing");
                     collapseStack();
                 } else {
                     // App bubble is not selected, select it
+                    Log.v(TAG, "  showOrHideAppBubble, expanded, selecting existing app bubble");
                     mBubbleData.setSelectedBubble(existingAppBubble);
                 }
             } else {
                 // App bubble is not selected, select it & expand
+                Log.v(TAG, "  showOrHideAppBubble, expand and select existing app bubble");
                 mBubbleData.setSelectedBubble(existingAppBubble);
                 mBubbleData.setExpanded(true);
             }
         } else {
             // App bubble does not exist, lets add and expand it
+            Log.v(TAG, "  showOrHideAppBubble, creating and expanding app bubble");
             Bubble b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
             b.setShouldAutoExpand(true);
             inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
index 1c94625..54e162b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
@@ -54,6 +54,7 @@
     // Referenced in com.android.systemui.util.NotificationChannels.
     public static final String NOTIFICATION_CHANNEL = "TVPIP";
     private static final String NOTIFICATION_TAG = "TvPip";
+    private static final String EXTRA_COMPONENT_NAME = "TvPipComponentName";
 
     private final Context mContext;
     private final PackageManager mPackageManager;
@@ -176,6 +177,7 @@
 
         Bundle extras = new Bundle();
         extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken);
+        extras.putParcelable(EXTRA_COMPONENT_NAME, PipUtils.getTopPipActivity(mContext).first);
         mNotificationBuilder.setExtras(extras);
 
         PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index e6418f3..1a0c011 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -135,7 +135,6 @@
         } catch (RemoteException e) {
             snapshotSurface.clearWindowSynced();
         }
-        window.setOuter(snapshotSurface);
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
             session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0,
@@ -161,7 +160,7 @@
             ShellExecutor splashScreenExecutor) {
         mSplashScreenExecutor = splashScreenExecutor;
         mSession = WindowManagerGlobal.getWindowSession();
-        mWindow = new Window();
+        mWindow = new Window(this);
         mWindow.setSession(mSession);
         int backgroundColor = taskDescription.getBackgroundColor();
         mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE);
@@ -204,9 +203,9 @@
     }
 
     static class Window extends BaseIWindow {
-        private WeakReference<TaskSnapshotWindow> mOuter;
+        private final WeakReference<TaskSnapshotWindow> mOuter;
 
-        public void setOuter(TaskSnapshotWindow outer) {
+        Window(TaskSnapshotWindow outer) {
             mOuter = new WeakReference<>(outer);
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index bf783e6..8c2203e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -21,17 +21,9 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
 import static android.view.WindowManager.TRANSIT_PIP;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
-import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
-import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
-import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
-import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
-import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
 import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
 
 import android.annotation.NonNull;
@@ -56,7 +48,6 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
-import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.splitscreen.StageCoordinator;
 import com.android.wm.shell.sysui.ShellInit;
@@ -84,7 +75,7 @@
     private UnfoldTransitionHandler mUnfoldHandler;
     private ActivityEmbeddingController mActivityEmbeddingController;
 
-    private static class MixedTransition {
+    abstract static class MixedTransition {
         static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
 
         /** Both the display and split-state (enter/exit) is changing */
@@ -124,15 +115,11 @@
         int mAnimType = ANIM_TYPE_DEFAULT;
         final IBinder mTransition;
 
-        private final Transitions mPlayer;
-        private final DefaultMixedHandler mMixedHandler;
-        private final PipTransitionController mPipHandler;
-        private final RecentsTransitionHandler mRecentsHandler;
-        private final StageCoordinator mSplitHandler;
-        private final KeyguardTransitionHandler mKeyguardHandler;
-        private final DesktopTasksController mDesktopTasksController;
-        private final UnfoldTransitionHandler mUnfoldHandler;
-        private final ActivityEmbeddingController mActivityEmbeddingController;
+        protected final Transitions mPlayer;
+        protected final DefaultMixedHandler mMixedHandler;
+        protected final PipTransitionController mPipHandler;
+        protected final StageCoordinator mSplitHandler;
+        protected final KeyguardTransitionHandler mKeyguardHandler;
 
         Transitions.TransitionHandler mLeftoversHandler = null;
         TransitionInfo mInfo = null;
@@ -156,409 +143,33 @@
 
         MixedTransition(int type, IBinder transition, Transitions player,
                 DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
-                RecentsTransitionHandler recentsHandler, StageCoordinator splitHandler,
-                KeyguardTransitionHandler keyguardHandler,
-                DesktopTasksController desktopTasksController,
-                UnfoldTransitionHandler unfoldHandler,
-                ActivityEmbeddingController activityEmbeddingController) {
+                StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler) {
             mType = type;
             mTransition = transition;
             mPlayer = player;
             mMixedHandler = mixedHandler;
             mPipHandler = pipHandler;
-            mRecentsHandler = recentsHandler;
             mSplitHandler = splitHandler;
             mKeyguardHandler = keyguardHandler;
-            mDesktopTasksController = desktopTasksController;
-            mUnfoldHandler = unfoldHandler;
-            mActivityEmbeddingController = activityEmbeddingController;
-
-            switch (type) {
-                case TYPE_RECENTS_DURING_DESKTOP:
-                case TYPE_RECENTS_DURING_KEYGUARD:
-                case TYPE_RECENTS_DURING_SPLIT:
-                    mLeftoversHandler = mRecentsHandler;
-                    break;
-                case TYPE_UNFOLD:
-                    mLeftoversHandler = mUnfoldHandler;
-                    break;
-                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
-                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
-                case TYPE_ENTER_PIP_FROM_SPLIT:
-                case TYPE_KEYGUARD:
-                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
-                default:
-                    break;
-            }
         }
 
-        boolean startAnimation(
+        abstract boolean startAnimation(
                 @NonNull IBinder transition, @NonNull TransitionInfo info,
                 @NonNull SurfaceControl.Transaction startTransaction,
                 @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            switch (mType) {
-                case TYPE_ENTER_PIP_FROM_SPLIT:
-                    return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
-                            finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
-                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
-                    return animateEnterPipFromActivityEmbedding(
-                            info, startTransaction, finishTransaction, finishCallback);
-                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
-                    return false;
-                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
-                    final boolean handledToPip = animateOpenIntentWithRemoteAndPip(
-                            info, startTransaction, finishTransaction, finishCallback);
-                    // Consume the transition on remote handler if the leftover handler already
-                    // handle this transition. And if it cannot, the transition will be handled by
-                    // remote handler, so don't consume here.
-                    // Need to check leftOverHandler as it may change in
-                    // #animateOpenIntentWithRemoteAndPip
-                    if (handledToPip && mHasRequestToRemote
-                            && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
-                        mPlayer.getRemoteTransitionHandler().onTransitionConsumed(
-                                transition, false, null);
-                    }
-                    return handledToPip;
-                case TYPE_RECENTS_DURING_SPLIT:
-                    for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                        final TransitionInfo.Change change = info.getChanges().get(i);
-                        // Pip auto-entering info might be appended to recent transition like
-                        // pressing home-key in 3-button navigation. This offers split handler the
-                        // opportunity to handle split to pip animation.
-                        if (mPipHandler.isEnteringPip(change, info.getType())
-                                && mSplitHandler.getSplitItemPosition(change.getLastParent())
-                                != SPLIT_POSITION_UNDEFINED) {
-                            return animateEnterPipFromSplit(
-                                    this, info, startTransaction, finishTransaction, finishCallback,
-                                    mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
-                        }
-                    }
+                @NonNull Transitions.TransitionFinishCallback finishCallback);
 
-                    return animateRecentsDuringSplit(
-                            info, startTransaction, finishTransaction, finishCallback);
-                case TYPE_KEYGUARD:
-                    return animateKeyguard(this, info, startTransaction, finishTransaction,
-                            finishCallback, mKeyguardHandler, mPipHandler);
-                case TYPE_RECENTS_DURING_KEYGUARD:
-                    return animateRecentsDuringKeyguard(
-                            info, startTransaction, finishTransaction, finishCallback);
-                case TYPE_RECENTS_DURING_DESKTOP:
-                    return animateRecentsDuringDesktop(
-                            info, startTransaction, finishTransaction, finishCallback);
-                case TYPE_UNFOLD:
-                    return animateUnfold(
-                            info, startTransaction, finishTransaction, finishCallback);
-                default:
-                    throw new IllegalStateException(
-                            "Starting mixed animation without a known mixed type? " + mType);
-            }
-        }
-
-        private boolean animateEnterPipFromActivityEmbedding(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
-                    + "entering PIP from an Activity Embedding window");
-            // Split into two transitions (wct)
-            TransitionInfo.Change pipChange = null;
-            final TransitionInfo everythingElse =
-                    subCopy(info, TRANSIT_TO_BACK, true /* changes */);
-            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                TransitionInfo.Change change = info.getChanges().get(i);
-                if (mPipHandler.isEnteringPip(change, info.getType())) {
-                    if (pipChange != null) {
-                        throw new IllegalStateException("More than 1 pip-entering changes in one"
-                                + " transition? " + info);
-                    }
-                    pipChange = change;
-                    // going backwards, so remove-by-index is fine.
-                    everythingElse.getChanges().remove(i);
-                }
-            }
-
-            final Transitions.TransitionFinishCallback finishCB = (wct) -> {
-                --mInFlightSubAnimations;
-                joinFinishArgs(wct);
-                if (mInFlightSubAnimations > 0) return;
-                finishCallback.onTransitionFinished(mFinishWCT);
-            };
-
-            if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) {
-                // Fallback to dispatching to other handlers.
-                return false;
-            }
-
-            // PIP window should always be on the highest Z order.
-            if (pipChange != null) {
-                mInFlightSubAnimations = 2;
-                mPipHandler.startEnterAnimation(
-                        pipChange,
-                        startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE),
-                        finishTransaction,
-                        finishCB);
-            } else {
-                mInFlightSubAnimations = 1;
-            }
-
-            mActivityEmbeddingController.startAnimation(mTransition, everythingElse,
-                    startTransaction, finishTransaction, finishCB);
-            return true;
-        }
-
-        private boolean animateOpenIntentWithRemoteAndPip(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            TransitionInfo.Change pipChange = null;
-            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                TransitionInfo.Change change = info.getChanges().get(i);
-                if (mPipHandler.isEnteringPip(change, info.getType())) {
-                    if (pipChange != null) {
-                        throw new IllegalStateException("More than 1 pip-entering changes in one"
-                                + " transition? " + info);
-                    }
-                    pipChange = change;
-                    info.getChanges().remove(i);
-                }
-            }
-            Transitions.TransitionFinishCallback finishCB = (wct) -> {
-                --mInFlightSubAnimations;
-                joinFinishArgs(wct);
-                if (mInFlightSubAnimations > 0) return;
-                finishCallback.onTransitionFinished(mFinishWCT);
-            };
-            if (pipChange == null) {
-                if (mLeftoversHandler != null) {
-                    mInFlightSubAnimations = 1;
-                    if (mLeftoversHandler.startAnimation(
-                            mTransition, info, startTransaction, finishTransaction, finishCB)) {
-                        return true;
-                    }
-                }
-                return false;
-            }
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
-                    + " animation because remote-animation likely doesn't support it");
-            // Split the transition into 2 parts: the pip part and the rest.
-            mInFlightSubAnimations = 2;
-            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
-            // we need a separate one to send over to launcher.
-            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
-
-            mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
-
-            // Dispatch the rest of the transition normally.
-            if (mLeftoversHandler != null
-                    && mLeftoversHandler.startAnimation(
-                            mTransition, info, startTransaction, finishTransaction, finishCB)) {
-                return true;
-            }
-            mLeftoversHandler = mPlayer.dispatchTransition(mTransition, info,
-                    startTransaction, finishTransaction, finishCB, mMixedHandler);
-            return true;
-        }
-
-        private boolean animateRecentsDuringSplit(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            // Split-screen is only interested in the recents transition finishing (and merging), so
-            // just wrap finish and start recents animation directly.
-            Transitions.TransitionFinishCallback finishCB = (wct) -> {
-                mInFlightSubAnimations = 0;
-                // If pair-to-pair switching, the post-recents clean-up isn't needed.
-                wct = wct != null ? wct : new WindowContainerTransaction();
-                if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
-                    mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
-                } else {
-                    // notify pair-to-pair recents animation finish
-                    mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
-                }
-                mSplitHandler.onTransitionAnimationComplete();
-                finishCallback.onTransitionFinished(wct);
-            };
-            mInFlightSubAnimations = 1;
-            mSplitHandler.onRecentsInSplitAnimationStart(info);
-            final boolean handled = mLeftoversHandler.startAnimation(mTransition, info,
-                    startTransaction, finishTransaction, finishCB);
-            if (!handled) {
-                mSplitHandler.onRecentsInSplitAnimationCanceled();
-            }
-            return handled;
-        }
-
-        private boolean animateRecentsDuringKeyguard(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            if (mInfo == null) {
-                mInfo = info;
-                mFinishT = finishTransaction;
-                mFinishCB = finishCallback;
-            }
-            return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
-        }
-
-        private boolean animateRecentsDuringDesktop(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            Transitions.TransitionFinishCallback finishCB = wct -> {
-                mInFlightSubAnimations--;
-                if (mInFlightSubAnimations == 0) {
-                    finishCallback.onTransitionFinished(wct);
-                }
-            };
-
-            mInFlightSubAnimations++;
-            boolean consumed = mRecentsHandler.startAnimation(
-                    mTransition, info, startTransaction, finishTransaction, finishCB);
-            if (!consumed) {
-                mInFlightSubAnimations--;
-                return false;
-            }
-            if (mDesktopTasksController != null) {
-                mDesktopTasksController.syncSurfaceState(info, finishTransaction);
-                return true;
-            }
-
-            return false;
-        }
-
-        private boolean animateUnfold(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            final Transitions.TransitionFinishCallback finishCB = (wct) -> {
-                mInFlightSubAnimations--;
-                if (mInFlightSubAnimations > 0) return;
-                finishCallback.onTransitionFinished(wct);
-            };
-            mInFlightSubAnimations = 1;
-            // Sync pip state.
-            if (mPipHandler != null) {
-                mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
-            }
-            if (mSplitHandler != null && mSplitHandler.isSplitActive()) {
-                mSplitHandler.updateSurfaces(startTransaction);
-            }
-            return mUnfoldHandler.startAnimation(
-                    mTransition, info, startTransaction, finishTransaction, finishCB);
-        }
-
-        void mergeAnimation(
+        abstract void mergeAnimation(
                 @NonNull IBinder transition, @NonNull TransitionInfo info,
                 @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            switch (mType) {
-                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
-                    // queue since no actual animation.
-                    break;
-                case TYPE_ENTER_PIP_FROM_SPLIT:
-                    if (mAnimType == ANIM_TYPE_GOING_HOME) {
-                        boolean ended = mSplitHandler.end();
-                        // If split couldn't end (because it is remote), then don't end everything
-                        // else since we have to play out the animation anyways.
-                        if (!ended) return;
-                        mPipHandler.end();
-                        if (mLeftoversHandler != null) {
-                            mLeftoversHandler.mergeAnimation(
-                                    transition, info, t, mergeTarget, finishCallback);
-                        }
-                    } else {
-                        mPipHandler.end();
-                    }
-                    break;
-                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
-                    mPipHandler.end();
-                    mActivityEmbeddingController.mergeAnimation(transition, info, t, mergeTarget,
-                            finishCallback);
-                    break;
-                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
-                    mPipHandler.end();
-                    if (mLeftoversHandler != null) {
-                        mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
-                                finishCallback);
-                    }
-                    break;
-                case TYPE_RECENTS_DURING_SPLIT:
-                    if (mSplitHandler.isPendingEnter(transition)) {
-                        // Recents -> enter-split means that we are switching from one pair to
-                        // another pair.
-                        mAnimType = ANIM_TYPE_PAIR_TO_PAIR;
-                    }
-                    mLeftoversHandler.mergeAnimation(
-                            transition, info, t, mergeTarget, finishCallback);
-                    break;
-                case TYPE_KEYGUARD:
-                    mKeyguardHandler.mergeAnimation(
-                            transition, info, t, mergeTarget, finishCallback);
-                    break;
-                case TYPE_RECENTS_DURING_KEYGUARD:
-                    if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
-                        DefaultMixedHandler.handoverTransitionLeashes(mInfo, info, t, mFinishT);
-                        if (animateKeyguard(this, info, t, mFinishT, mFinishCB, mKeyguardHandler,
-                                mPipHandler)) {
-                            finishCallback.onTransitionFinished(null);
-                        }
-                    }
-                    mLeftoversHandler.mergeAnimation(
-                            transition, info, t, mergeTarget, finishCallback);
-                    break;
-                case TYPE_RECENTS_DURING_DESKTOP:
-                    mLeftoversHandler.mergeAnimation(
-                            transition, info, t, mergeTarget, finishCallback);
-                    break;
-                case TYPE_UNFOLD:
-                    mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
-                    break;
-                default:
-                    throw new IllegalStateException(
-                            "Playing a mixed transition with unknown type? " + mType);
-            }
-        }
+                @NonNull Transitions.TransitionFinishCallback finishCallback);
 
-        void onTransitionConsumed(
+        abstract void onTransitionConsumed(
                 @NonNull IBinder transition, boolean aborted,
-                @Nullable SurfaceControl.Transaction finishT) {
-            switch (mType) {
-                case TYPE_ENTER_PIP_FROM_SPLIT:
-                    mPipHandler.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
-                    mPipHandler.onTransitionConsumed(transition, aborted, finishT);
-                    mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                case TYPE_RECENTS_DURING_SPLIT:
-                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
-                case TYPE_RECENTS_DURING_DESKTOP:
-                    mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                case TYPE_KEYGUARD:
-                    mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                case TYPE_UNFOLD:
-                    mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                default:
-                    break;
-            }
+                @Nullable SurfaceControl.Transaction finishT);
 
-            if (mHasRequestToRemote) {
-                mPlayer.getRemoteTransitionHandler().onTransitionConsumed(
-                        transition, aborted, finishT);
-            }
-        }
-
-        boolean startSubAnimation(Transitions.TransitionHandler handler, TransitionInfo info,
+        protected boolean startSubAnimation(
+                Transitions.TransitionHandler handler, TransitionInfo info,
                 SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
             if (mInfo != null) {
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
@@ -573,7 +184,7 @@
             return true;
         }
 
-        void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) {
+        private void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) {
             mInFlightSubAnimations--;
             if (mInfo != null) {
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
@@ -644,7 +255,7 @@
                 throw new IllegalStateException("Unexpected remote transition in"
                         + "pip-enter-from-split request");
             }
-            mActiveTransitions.add(createMixedTransition(
+            mActiveTransitions.add(createDefaultMixedTransition(
                     MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition));
 
             WindowContainerTransaction out = new WindowContainerTransaction();
@@ -656,7 +267,7 @@
                 mActivityEmbeddingController != null)) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                     " Got a PiP-enter request from an Activity Embedding split");
-            mActiveTransitions.add(createMixedTransition(
+            mActiveTransitions.add(createDefaultMixedTransition(
                     MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING, transition));
             // Postpone transition splitting to later.
             WindowContainerTransaction out = new WindowContainerTransaction();
@@ -675,7 +286,7 @@
             if (handler == null) {
                 return null;
             }
-            final MixedTransition mixed = createMixedTransition(
+            final MixedTransition mixed = createDefaultMixedTransition(
                     MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
@@ -701,7 +312,7 @@
                         mPlayer.getRemoteTransitionHandler(),
                         new WindowContainerTransaction());
             }
-            final MixedTransition mixed = createMixedTransition(
+            final MixedTransition mixed = createRecentsMixedTransition(
                     MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
@@ -710,7 +321,7 @@
             final WindowContainerTransaction wct =
                     mUnfoldHandler.handleRequest(transition, request);
             if (wct != null) {
-                mActiveTransitions.add(createMixedTransition(
+                mActiveTransitions.add(createDefaultMixedTransition(
                         MixedTransition.TYPE_UNFOLD, transition));
             }
             return wct;
@@ -718,6 +329,12 @@
         return null;
     }
 
+    private DefaultMixedTransition createDefaultMixedTransition(int type, IBinder transition) {
+        return new DefaultMixedTransition(
+                type, transition, mPlayer, this, mPipHandler, mSplitHandler, mKeyguardHandler,
+                mUnfoldHandler, mActivityEmbeddingController);
+    }
+
     @Override
     public Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT) {
         if (mRecentsHandler != null) {
@@ -737,31 +354,30 @@
     private void setRecentsTransitionDuringSplit(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "Split-Screen is foreground, so treat it as Mixed.");
-        mActiveTransitions.add(createMixedTransition(
+        mActiveTransitions.add(createRecentsMixedTransition(
                 MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition));
     }
 
     private void setRecentsTransitionDuringKeyguard(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "keyguard is visible, so treat it as Mixed.");
-        mActiveTransitions.add(createMixedTransition(
+        mActiveTransitions.add(createRecentsMixedTransition(
                 MixedTransition.TYPE_RECENTS_DURING_KEYGUARD, transition));
     }
 
     private void setRecentsTransitionDuringDesktop(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "desktop mode is active, so treat it as Mixed.");
-        mActiveTransitions.add(createMixedTransition(
+        mActiveTransitions.add(createRecentsMixedTransition(
                 MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition));
     }
 
-    private MixedTransition createMixedTransition(int type, IBinder transition) {
-        return new MixedTransition(type, transition, mPlayer, this, mPipHandler, mRecentsHandler,
-                mSplitHandler, mKeyguardHandler, mDesktopTasksController, mUnfoldHandler,
-                mActivityEmbeddingController);
+    private MixedTransition createRecentsMixedTransition(int type, IBinder transition) {
+        return new RecentsMixedTransition(type, transition, mPlayer, this, mPipHandler,
+                mSplitHandler, mKeyguardHandler, mRecentsHandler, mDesktopTasksController);
     }
 
-    private static TransitionInfo subCopy(@NonNull TransitionInfo info,
+    static TransitionInfo subCopy(@NonNull TransitionInfo info,
             @WindowManager.TransitionType int newType, boolean withChanges) {
         final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
         out.setTrack(info.getTrack());
@@ -778,15 +394,6 @@
         return out;
     }
 
-    private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
-        return change.getTaskInfo() != null
-                && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
-    }
-
-    private static boolean isWallpaper(@NonNull TransitionInfo.Change change) {
-        return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
-    }
-
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
@@ -805,7 +412,7 @@
         if (KeyguardTransitionHandler.handles(info)) {
             if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) {
                 final MixedTransition keyguardMixed =
-                        createMixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
+                        createDefaultMixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
                 mActiveTransitions.add(keyguardMixed);
                 Transitions.TransitionFinishCallback callback = wct -> {
                     mActiveTransitions.remove(keyguardMixed);
@@ -845,117 +452,6 @@
         return handled;
     }
 
-    private static boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed,
-            @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback,
-            @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler,
-            @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) {
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
-                + "entering PIP while Split-Screen is foreground.");
-        TransitionInfo.Change pipChange = null;
-        TransitionInfo.Change wallpaper = null;
-        final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
-        boolean homeIsOpening = false;
-        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-            TransitionInfo.Change change = info.getChanges().get(i);
-            if (pipHandler.isEnteringPip(change, info.getType())) {
-                if (pipChange != null) {
-                    throw new IllegalStateException("More than 1 pip-entering changes in one"
-                            + " transition? " + info);
-                }
-                pipChange = change;
-                // going backwards, so remove-by-index is fine.
-                everythingElse.getChanges().remove(i);
-            } else if (isHomeOpening(change)) {
-                homeIsOpening = true;
-            } else if (isWallpaper(change)) {
-                wallpaper = change;
-            }
-        }
-        if (pipChange == null) {
-            // um, something probably went wrong.
-            return false;
-        }
-        final boolean isGoingHome = homeIsOpening;
-        Transitions.TransitionFinishCallback finishCB = (wct) -> {
-            --mixed.mInFlightSubAnimations;
-            mixed.joinFinishArgs(wct);
-            if (mixed.mInFlightSubAnimations > 0) return;
-            if (isGoingHome) {
-                splitHandler.onTransitionAnimationComplete();
-            }
-            finishCallback.onTransitionFinished(mixed.mFinishWCT);
-        };
-        if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent())
-                != SPLIT_POSITION_UNDEFINED) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
-                    + "since entering-PiP caused us to leave split and return home.");
-            // We need to split the transition into 2 parts: the pip part (animated by pip)
-            // and the dismiss-part (animated by launcher).
-            mixed.mInFlightSubAnimations = 2;
-            // immediately make the wallpaper visible (so that we don't see it pop-in during
-            // the time it takes to start recents animation (which is remote).
-            if (wallpaper != null) {
-                startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f);
-            }
-            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
-            // we need a separate one to send over to launcher.
-            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
-            @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED;
-            if (splitHandler.isSplitScreenVisible()) {
-                // The non-going home case, we could be pip-ing one of the split stages and keep
-                // showing the other
-                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                    TransitionInfo.Change change = info.getChanges().get(i);
-                    if (change == pipChange) {
-                        // Ignore the change/task that's going into Pip
-                        continue;
-                    }
-                    @SplitScreen.StageType int splitItemStage =
-                            splitHandler.getSplitItemStage(change.getLastParent());
-                    if (splitItemStage != STAGE_TYPE_UNDEFINED) {
-                        topStageToKeep = splitItemStage;
-                        break;
-                    }
-                }
-            }
-            // Let split update internal state for dismiss.
-            splitHandler.prepareDismissAnimation(topStageToKeep,
-                    EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
-                    finishTransaction);
-
-            // We are trying to accommodate launcher's close animation which can't handle the
-            // divider-bar, so if split-handler is closing the divider-bar, just hide it and remove
-            // from transition info.
-            for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) {
-                if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) != 0) {
-                    everythingElse.getChanges().remove(i);
-                    break;
-                }
-            }
-
-            pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
-            pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
-                    finishCB);
-            // Dispatch the rest of the transition normally. This will most-likely be taken by
-            // recents or default handler.
-            mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
-                    otherStartT, finishTransaction, finishCB, mixedHandler);
-        } else {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Not leaving split, so just "
-                    + "forward animation to Pip-Handler.");
-            // This happens if the pip-ing activity is in a multi-activity task (and thus a
-            // new pip task is spawned). In this case, we don't actually exit split so we can
-            // just let pip transition handle the animation verbatim.
-            mixed.mInFlightSubAnimations = 1;
-            pipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction,
-                    finishCB);
-        }
-        return true;
-    }
-
     private void unlinkMissingParents(TransitionInfo from) {
         for (int i = 0; i < from.getChanges().size(); ++i) {
             final TransitionInfo.Change chg = from.getChanges().get(i);
@@ -987,15 +483,14 @@
     public boolean animatePendingEnterPipFromSplit(IBinder transition, TransitionInfo info,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             Transitions.TransitionFinishCallback finishCallback) {
-        final MixedTransition mixed = createMixedTransition(
+        final MixedTransition mixed = createDefaultMixedTransition(
                 MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition);
         mActiveTransitions.add(mixed);
         Transitions.TransitionFinishCallback callback = wct -> {
             mActiveTransitions.remove(mixed);
             finishCallback.onTransitionFinished(wct);
         };
-        return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback, mPlayer, this,
-                mPipHandler, mSplitHandler);
+        return mixed.startAnimation(transition, info, startT, finishT, callback);
     }
 
     /**
@@ -1018,7 +513,7 @@
         }
         if (displayPart.getChanges().isEmpty()) return false;
         unlinkMissingParents(everythingElse);
-        final MixedTransition mixed = createMixedTransition(
+        final MixedTransition mixed = createDefaultMixedTransition(
                 MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition);
         mActiveTransitions.add(mixed);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change "
@@ -1135,7 +630,7 @@
      * {@link TransitionInfo} so that it can take over some parts of the animation without
      * reparenting to new transition roots.
      */
-    private static void handoverTransitionLeashes(
+    static void handoverTransitionLeashes(
             @NonNull TransitionInfo from,
             @NonNull TransitionInfo to,
             @NonNull SurfaceControl.Transaction startT,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
new file mode 100644
index 0000000..9ce46d6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
@@ -0,0 +1,315 @@
+/*
+ * 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.wm.shell.transition;
+
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+
+import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+import com.android.wm.shell.unfold.UnfoldTransitionHandler;
+
+class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
+    private final UnfoldTransitionHandler mUnfoldHandler;
+    private final ActivityEmbeddingController mActivityEmbeddingController;
+
+    DefaultMixedTransition(int type, IBinder transition, Transitions player,
+            DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+            StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
+            UnfoldTransitionHandler unfoldHandler,
+            ActivityEmbeddingController activityEmbeddingController) {
+        super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler);
+        mUnfoldHandler = unfoldHandler;
+        mActivityEmbeddingController = activityEmbeddingController;
+
+        switch (type) {
+            case TYPE_UNFOLD:
+                mLeftoversHandler = mUnfoldHandler;
+                break;
+            case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+            case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+            case TYPE_ENTER_PIP_FROM_SPLIT:
+            case TYPE_KEYGUARD:
+            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+            default:
+                break;
+        }
+    }
+
+    @Override
+    boolean startAnimation(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        return switch (mType) {
+            case TYPE_DISPLAY_AND_SPLIT_CHANGE -> false;
+            case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING ->
+                    animateEnterPipFromActivityEmbedding(
+                            info, startTransaction, finishTransaction, finishCallback);
+            case TYPE_ENTER_PIP_FROM_SPLIT ->
+                    animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
+                            finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+            case TYPE_KEYGUARD ->
+                    animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback,
+                            mKeyguardHandler, mPipHandler);
+            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE ->
+                    animateOpenIntentWithRemoteAndPip(transition, info, startTransaction,
+                            finishTransaction, finishCallback);
+            case TYPE_UNFOLD ->
+                    animateUnfold(info, startTransaction, finishTransaction, finishCallback);
+            default -> throw new IllegalStateException(
+                    "Starting default mixed animation with unknown or illegal type: " + mType);
+        };
+    }
+
+    private boolean animateEnterPipFromActivityEmbedding(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+                + "entering PIP from an Activity Embedding window");
+        // Split into two transitions (wct)
+        TransitionInfo.Change pipChange = null;
+        final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (mPipHandler.isEnteringPip(change, info.getType())) {
+                if (pipChange != null) {
+                    throw new IllegalStateException("More than 1 pip-entering changes in one"
+                            + " transition? " + info);
+                }
+                pipChange = change;
+                // going backwards, so remove-by-index is fine.
+                everythingElse.getChanges().remove(i);
+            }
+        }
+
+        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            --mInFlightSubAnimations;
+            joinFinishArgs(wct);
+            if (mInFlightSubAnimations > 0) return;
+            finishCallback.onTransitionFinished(mFinishWCT);
+        };
+
+        if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) {
+            // Fallback to dispatching to other handlers.
+            return false;
+        }
+
+        // PIP window should always be on the highest Z order.
+        if (pipChange != null) {
+            mInFlightSubAnimations = 2;
+            mPipHandler.startEnterAnimation(
+                    pipChange, startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE),
+                    finishTransaction,
+                    finishCB);
+        } else {
+            mInFlightSubAnimations = 1;
+        }
+
+        mActivityEmbeddingController.startAnimation(
+                mTransition, everythingElse, startTransaction, finishTransaction, finishCB);
+        return true;
+    }
+
+    private boolean animateOpenIntentWithRemoteAndPip(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip(
+                info, startTransaction, finishTransaction, finishCallback);
+        // Consume the transition on remote handler if the leftover handler already handle this
+        // transition. And if it cannot, the transition will be handled by remote handler, so don't
+        // consume here.
+        // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
+        if (handledToPip && mHasRequestToRemote
+                && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+            mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
+        }
+        return handledToPip;
+    }
+
+    private boolean tryAnimateOpenIntentWithRemoteAndPip(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        TransitionInfo.Change pipChange = null;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (mPipHandler.isEnteringPip(change, info.getType())) {
+                if (pipChange != null) {
+                    throw new IllegalStateException("More than 1 pip-entering changes in one"
+                            + " transition? " + info);
+                }
+                pipChange = change;
+                info.getChanges().remove(i);
+            }
+        }
+        Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            --mInFlightSubAnimations;
+            joinFinishArgs(wct);
+            if (mInFlightSubAnimations > 0) return;
+            finishCallback.onTransitionFinished(mFinishWCT);
+        };
+        if (pipChange == null) {
+            if (mLeftoversHandler != null) {
+                mInFlightSubAnimations = 1;
+                if (mLeftoversHandler.startAnimation(
+                        mTransition, info, startTransaction, finishTransaction, finishCB)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
+                + " animation because remote-animation likely doesn't support it");
+        // Split the transition into 2 parts: the pip part and the rest.
+        mInFlightSubAnimations = 2;
+        // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+        // we need a separate one to send over to launcher.
+        SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+
+        mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
+
+        // Dispatch the rest of the transition normally.
+        if (mLeftoversHandler != null
+                && mLeftoversHandler.startAnimation(mTransition, info,
+                startTransaction, finishTransaction, finishCB)) {
+            return true;
+        }
+        mLeftoversHandler = mPlayer.dispatchTransition(
+                mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler);
+        return true;
+    }
+
+    private boolean animateUnfold(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            mInFlightSubAnimations--;
+            if (mInFlightSubAnimations > 0) return;
+            finishCallback.onTransitionFinished(wct);
+        };
+        mInFlightSubAnimations = 1;
+        // Sync pip state.
+        if (mPipHandler != null) {
+            mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+        }
+        if (mSplitHandler != null && mSplitHandler.isSplitActive()) {
+            mSplitHandler.updateSurfaces(startTransaction);
+        }
+        return mUnfoldHandler.startAnimation(
+                mTransition, info, startTransaction, finishTransaction, finishCB);
+    }
+
+    @Override
+    void mergeAnimation(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        switch (mType) {
+            case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+                // queue since no actual animation.
+                return;
+            case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+                mPipHandler.end();
+                mActivityEmbeddingController.mergeAnimation(
+                        transition, info, t, mergeTarget, finishCallback);
+                return;
+            case TYPE_ENTER_PIP_FROM_SPLIT:
+                if (mAnimType == ANIM_TYPE_GOING_HOME) {
+                    boolean ended = mSplitHandler.end();
+                    // If split couldn't end (because it is remote), then don't end everything else
+                    // since we have to play out the animation anyways.
+                    if (!ended) return;
+                    mPipHandler.end();
+                    if (mLeftoversHandler != null) {
+                        mLeftoversHandler.mergeAnimation(
+                                transition, info, t, mergeTarget, finishCallback);
+                    }
+                } else {
+                    mPipHandler.end();
+                }
+                return;
+            case TYPE_KEYGUARD:
+                mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                return;
+            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+                mPipHandler.end();
+                if (mLeftoversHandler != null) {
+                    mLeftoversHandler.mergeAnimation(
+                            transition, info, t, mergeTarget, finishCallback);
+                }
+                return;
+            case TYPE_UNFOLD:
+                mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                return;
+            default:
+                throw new IllegalStateException("Playing a default mixed transition with unknown or"
+                        + " illegal type: " + mType);
+        }
+    }
+
+    @Override
+    void onTransitionConsumed(
+            @NonNull IBinder transition, boolean aborted,
+            @Nullable SurfaceControl.Transaction finishT) {
+        switch (mType) {
+            case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+                mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+                mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            case TYPE_ENTER_PIP_FROM_SPLIT:
+                mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            case TYPE_KEYGUARD:
+                mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+                mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            case TYPE_UNFOLD:
+                mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            default:
+                break;
+        }
+
+        if (mHasRequestToRemote) {
+            mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
new file mode 100644
index 0000000..0974cd1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
@@ -0,0 +1,181 @@
+/*
+ * 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.wm.shell.transition;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
+import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy;
+
+import android.annotation.NonNull;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.SplitScreen;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+
+public class MixedTransitionHelper {
+    static boolean animateEnterPipFromSplit(
+            @NonNull DefaultMixedHandler.MixedTransition mixed, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler,
+            @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+                + "entering PIP while Split-Screen is foreground.");
+        TransitionInfo.Change pipChange = null;
+        TransitionInfo.Change wallpaper = null;
+        final TransitionInfo everythingElse =
+                subCopy(info, TRANSIT_TO_BACK, true /* changes */);
+        boolean homeIsOpening = false;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (pipHandler.isEnteringPip(change, info.getType())) {
+                if (pipChange != null) {
+                    throw new IllegalStateException("More than 1 pip-entering changes in one"
+                            + " transition? " + info);
+                }
+                pipChange = change;
+                // going backwards, so remove-by-index is fine.
+                everythingElse.getChanges().remove(i);
+            } else if (isHomeOpening(change)) {
+                homeIsOpening = true;
+            } else if (isWallpaper(change)) {
+                wallpaper = change;
+            }
+        }
+        if (pipChange == null) {
+            // um, something probably went wrong.
+            return false;
+        }
+        final boolean isGoingHome = homeIsOpening;
+        Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            --mixed.mInFlightSubAnimations;
+            mixed.joinFinishArgs(wct);
+            if (mixed.mInFlightSubAnimations > 0) return;
+            if (isGoingHome) {
+                splitHandler.onTransitionAnimationComplete();
+            }
+            finishCallback.onTransitionFinished(mixed.mFinishWCT);
+        };
+        if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent())
+                != SPLIT_POSITION_UNDEFINED) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
+                    + "since entering-PiP caused us to leave split and return home.");
+            // We need to split the transition into 2 parts: the pip part (animated by pip)
+            // and the dismiss-part (animated by launcher).
+            mixed.mInFlightSubAnimations = 2;
+            // immediately make the wallpaper visible (so that we don't see it pop-in during
+            // the time it takes to start recents animation (which is remote).
+            if (wallpaper != null) {
+                startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f);
+            }
+            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+            // we need a separate one to send over to launcher.
+            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+            @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED;
+            if (splitHandler.isSplitScreenVisible()) {
+                // The non-going home case, we could be pip-ing one of the split stages and keep
+                // showing the other
+                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                    TransitionInfo.Change change = info.getChanges().get(i);
+                    if (change == pipChange) {
+                        // Ignore the change/task that's going into Pip
+                        continue;
+                    }
+                    @SplitScreen.StageType int splitItemStage =
+                            splitHandler.getSplitItemStage(change.getLastParent());
+                    if (splitItemStage != STAGE_TYPE_UNDEFINED) {
+                        topStageToKeep = splitItemStage;
+                        break;
+                    }
+                }
+            }
+            // Let split update internal state for dismiss.
+            splitHandler.prepareDismissAnimation(topStageToKeep,
+                    EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
+                    finishTransaction);
+
+            // We are trying to accommodate launcher's close animation which can't handle the
+            // divider-bar, so if split-handler is closing the divider-bar, just hide it and
+            // remove from transition info.
+            for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) {
+                if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR)
+                        != 0) {
+                    everythingElse.getChanges().remove(i);
+                    break;
+                }
+            }
+
+            pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
+            pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
+                    finishCB);
+            // Dispatch the rest of the transition normally. This will most-likely be taken by
+            // recents or default handler.
+            mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
+                    otherStartT, finishTransaction, finishCB, mixedHandler);
+        } else {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Not leaving split, so just "
+                    + "forward animation to Pip-Handler.");
+            // This happens if the pip-ing activity is in a multi-activity task (and thus a
+            // new pip task is spawned). In this case, we don't actually exit split so we can
+            // just let pip transition handle the animation verbatim.
+            mixed.mInFlightSubAnimations = 1;
+            pipHandler.startAnimation(
+                    mixed.mTransition, info, startTransaction, finishTransaction, finishCB);
+        }
+        return true;
+    }
+
+    private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
+        return change.getTaskInfo() != null
+                && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
+    }
+
+    private static boolean isWallpaper(@NonNull TransitionInfo.Change change) {
+        return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
+    }
+
+    static boolean animateKeyguard(
+            @NonNull DefaultMixedHandler.MixedTransition mixed, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull KeyguardTransitionHandler keyguardHandler,
+            PipTransitionController pipHandler) {
+        if (mixed.mFinishT == null) {
+            mixed.mFinishT = finishTransaction;
+            mixed.mFinishCB = finishCallback;
+        }
+        // Sync pip state.
+        if (pipHandler != null) {
+            pipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+        }
+        return mixed.startSubAnimation(keyguardHandler, info, startTransaction, finishTransaction);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
new file mode 100644
index 0000000..643e026
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -0,0 +1,214 @@
+/*
+ * 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.wm.shell.transition;
+
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.transition.DefaultMixedHandler.handoverTransitionLeashes;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+
+class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
+    private final RecentsTransitionHandler mRecentsHandler;
+    private final DesktopTasksController mDesktopTasksController;
+
+    RecentsMixedTransition(int type, IBinder transition, Transitions player,
+            DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+            StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
+            RecentsTransitionHandler recentsHandler,
+            DesktopTasksController desktopTasksController) {
+        super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler);
+        mRecentsHandler = recentsHandler;
+        mDesktopTasksController = desktopTasksController;
+        mLeftoversHandler = mRecentsHandler;
+    }
+
+    @Override
+    boolean startAnimation(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        return switch (mType) {
+            case TYPE_RECENTS_DURING_DESKTOP ->
+                    animateRecentsDuringDesktop(
+                            info, startTransaction, finishTransaction, finishCallback);
+            case TYPE_RECENTS_DURING_KEYGUARD ->
+                    animateRecentsDuringKeyguard(
+                            info, startTransaction, finishTransaction, finishCallback);
+            case TYPE_RECENTS_DURING_SPLIT ->
+                    animateRecentsDuringSplit(
+                            info, startTransaction, finishTransaction, finishCallback);
+            default -> throw new IllegalStateException(
+                    "Starting Recents mixed animation with unknown or illegal type: " + mType);
+        };
+    }
+
+    private boolean animateRecentsDuringDesktop(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        if (mInfo == null) {
+            mInfo = info;
+            mFinishT = finishTransaction;
+            mFinishCB = finishCallback;
+        }
+        Transitions.TransitionFinishCallback finishCB = wct -> {
+            mInFlightSubAnimations--;
+            if (mInFlightSubAnimations == 0) {
+                finishCallback.onTransitionFinished(wct);
+            }
+        };
+
+        mInFlightSubAnimations++;
+        boolean consumed = mRecentsHandler.startAnimation(
+                mTransition, info, startTransaction, finishTransaction, finishCB);
+        if (!consumed) {
+            mInFlightSubAnimations--;
+            return false;
+        }
+        if (mDesktopTasksController != null) {
+            mDesktopTasksController.syncSurfaceState(info, finishTransaction);
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean animateRecentsDuringKeyguard(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        if (mInfo == null) {
+            mInfo = info;
+            mFinishT = finishTransaction;
+            mFinishCB = finishCallback;
+        }
+        return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
+    }
+
+    private boolean animateRecentsDuringSplit(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            // Pip auto-entering info might be appended to recent transition like pressing
+            // home-key in 3-button navigation. This offers split handler the opportunity to
+            // handle split to pip animation.
+            if (mPipHandler.isEnteringPip(change, info.getType())
+                    && mSplitHandler.getSplitItemPosition(change.getLastParent())
+                    != SPLIT_POSITION_UNDEFINED) {
+                return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
+                        finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+            }
+        }
+
+        // Split-screen is only interested in the recents transition finishing (and merging), so
+        // just wrap finish and start recents animation directly.
+        Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            mInFlightSubAnimations = 0;
+            // If pair-to-pair switching, the post-recents clean-up isn't needed.
+            wct = wct != null ? wct : new WindowContainerTransaction();
+            if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
+                mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
+            } else {
+                // notify pair-to-pair recents animation finish
+                mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
+            }
+            mSplitHandler.onTransitionAnimationComplete();
+            finishCallback.onTransitionFinished(wct);
+        };
+        mInFlightSubAnimations = 1;
+        mSplitHandler.onRecentsInSplitAnimationStart(info);
+        final boolean handled = mLeftoversHandler.startAnimation(
+                mTransition, info, startTransaction, finishTransaction, finishCB);
+        if (!handled) {
+            mSplitHandler.onRecentsInSplitAnimationCanceled();
+        }
+        return handled;
+    }
+
+    @Override
+    void mergeAnimation(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        switch (mType) {
+            case TYPE_RECENTS_DURING_DESKTOP:
+                mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                return;
+            case TYPE_RECENTS_DURING_KEYGUARD:
+                if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
+                    handoverTransitionLeashes(mInfo, info, t, mFinishT);
+                    if (animateKeyguard(
+                            this, info, t, mFinishT, mFinishCB, mKeyguardHandler, mPipHandler)) {
+                        finishCallback.onTransitionFinished(null);
+                    }
+                }
+                mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+                        finishCallback);
+                return;
+            case TYPE_RECENTS_DURING_SPLIT:
+                if (mSplitHandler.isPendingEnter(transition)) {
+                    // Recents -> enter-split means that we are switching from one pair to
+                    // another pair.
+                    mAnimType = DefaultMixedHandler.MixedTransition.ANIM_TYPE_PAIR_TO_PAIR;
+                }
+                mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                return;
+            default:
+                throw new IllegalStateException("Playing a Recents mixed transition with unknown or"
+                        + " illegal type: " + mType);
+        }
+    }
+
+    @Override
+    void onTransitionConsumed(
+            @NonNull IBinder transition, boolean aborted,
+            @Nullable SurfaceControl.Transaction finishT) {
+        switch (mType) {
+            case TYPE_RECENTS_DURING_DESKTOP:
+            case TYPE_RECENTS_DURING_SPLIT:
+                mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            default:
+                break;
+        }
+
+        if (mHasRequestToRemote) {
+            mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
index d7cb490..4aed7c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -16,6 +16,8 @@
 
 package com.android.wm.shell.unfold;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
 import android.annotation.NonNull;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
@@ -28,11 +30,11 @@
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
 import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
 
+import dagger.Lazy;
+
 import java.util.List;
 import java.util.Optional;
 
-import dagger.Lazy;
-
 /**
  * Manages fold/unfold animations of tasks on foldable devices.
  * When folding or unfolding a foldable device we play animations that
@@ -228,7 +230,8 @@
     }
 
     private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
-        if (!mIsInStageChange) {
+        // TODO(b/311084698): the windowing mode check is added here as a work around.
+        if (!mIsInStageChange || taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
             // No need to resetTask if there is no ongoing state change.
             return;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 41f8204..aabc1cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -421,9 +421,9 @@
             }
             moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
 
-            if (!mHasLongClicked) {
+            if (!mHasLongClicked && id != R.id.maximize_window) {
                 final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
-                decoration.closeMaximizeMenu();
+                decoration.closeMaximizeMenuIfNeeded(e);
             }
 
             final long eventDuration = e.getEventTime() - e.getDownTime();
@@ -643,7 +643,7 @@
                 handleCaptionThroughStatusBar(ev, relevantDecor);
             }
         }
-        handleEventOutsideFocusedCaption(ev, relevantDecor);
+        handleEventOutsideCaption(ev, relevantDecor);
         // Prevent status bar from reacting to a caption drag.
         if (DesktopModeStatus.isEnabled()) {
             if (mTransitionDragActive) {
@@ -652,11 +652,17 @@
         }
     }
 
-    // If an UP/CANCEL action is received outside of caption bounds, turn off handle menu
-    private void handleEventOutsideFocusedCaption(MotionEvent ev,
+    /**
+     * If an UP/CANCEL action is received outside of the caption bounds, close the handle and
+     * maximize the menu.
+     *
+     * @param relevantDecor the window decoration of the focused task's caption. This method only
+     *                      handles motion events outside this caption's bounds.
+     */
+    private void handleEventOutsideCaption(MotionEvent ev,
             DesktopModeWindowDecoration relevantDecor) {
         // Returns if event occurs within caption
-        if (relevantDecor == null || relevantDecor.checkTouchEventInCaptionHandle(ev)) {
+        if (relevantDecor == null || relevantDecor.checkTouchEventInCaption(ev)) {
             return;
         }
 
@@ -692,7 +698,7 @@
                     }
 
                     if (dragFromStatusBarAllowed
-                            && relevantDecor.checkTouchEventInCaptionHandle(ev)) {
+                            && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) {
                         mTransitionDragActive = true;
                     }
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 5f77192..53f806c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -612,8 +612,7 @@
     void closeMaximizeMenuIfNeeded(MotionEvent ev) {
         if (!isMaximizeMenuActive()) return;
 
-        final PointF inputPoint = offsetCaptionLocation(ev);
-        if (!mMaximizeMenu.isValidMenuInput(inputPoint)) {
+        if (!mMaximizeMenu.isValidMenuInput(ev)) {
             closeMaximizeMenu();
         }
     }
@@ -639,20 +638,34 @@
     }
 
     /**
-     * Checks if motion event occurs in the caption handle area. This should be used in cases where
+     * Checks if motion event occurs in the caption handle area of a focused caption (the caption on
+     * a task in fullscreen or in multi-windowing mode). This should be used in cases where
      * onTouchListener will not work (i.e. when caption is in status bar area).
      *
      * @param ev       the {@link MotionEvent} to check
-     * @return {@code true} if event is inside the specified view, {@code false} if not
+     * @return {@code true} if event is inside caption handle view, {@code false} if not
      */
-    boolean checkTouchEventInCaptionHandle(MotionEvent ev) {
+    boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) {
         if (isHandleMenuActive() || !(mWindowDecorViewHolder
                 instanceof DesktopModeFocusedWindowDecorationViewHolder)) {
             return false;
         }
+
+        return checkTouchEventInCaption(ev);
+    }
+
+    /**
+     * Checks if touch event occurs in caption.
+     *
+     * @param ev       the {@link MotionEvent} to check
+     * @return {@code true} if event is inside caption view, {@code false} if not
+     */
+    boolean checkTouchEventInCaption(MotionEvent ev) {
         final PointF inputPoint = offsetCaptionLocation(ev);
-        return ((DesktopModeFocusedWindowDecorationViewHolder) mWindowDecorViewHolder)
-                .pointInCaption(inputPoint, mResult.mCaptionX);
+        return inputPoint.x >= mResult.mCaptionX
+                && inputPoint.x <= mResult.mCaptionX + mResult.mCaptionWidth
+                && inputPoint.y >= 0
+                && inputPoint.y <= mResult.mCaptionHeight;
     }
 
     /**
@@ -668,7 +681,7 @@
             // Click if point in caption handle view
             final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
             final View handle = caption.findViewById(R.id.caption_handle);
-            if (checkTouchEventInCaptionHandle(ev)) {
+            if (checkTouchEventInFocusedCaptionHandle(ev)) {
                 mOnCaptionButtonClickListener.onClick(handle);
             }
         } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 921708f..794b357 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -22,10 +22,10 @@
 import android.graphics.PixelFormat
 import android.graphics.PointF
 import android.view.LayoutInflater
+import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.SurfaceControl.Transaction
 import android.view.SurfaceControlViewHost
-import android.view.View
 import android.view.View.OnClickListener
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
@@ -62,6 +62,8 @@
     private val cornerRadius = loadDimensionPixelSize(
             R.dimen.desktop_mode_maximize_menu_corner_radius
     ).toFloat()
+    private val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width)
+    private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
 
     /** Position the menu relative to the caption's position. */
     fun positionMenu(position: PointF, t: Transaction) {
@@ -95,8 +97,6 @@
                 .setName("Maximize Menu")
                 .setContainerLayer()
                 .build()
-        val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width)
-        val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
         val lp = WindowManager.LayoutParams(
                 menuWidth,
                 menuHeight,
@@ -160,14 +160,11 @@
      *
      * @param inputPoint the input to compare against.
      */
-    fun isValidMenuInput(inputPoint: PointF): Boolean {
-        val menuView = maximizeMenu?.mWindowViewHost?.view ?: return true
-        return !viewsLaidOut() || pointInView(menuView, inputPoint.x - menuPosition.x,
-                inputPoint.y - menuPosition.y)
-    }
-
-    private fun pointInView(v: View, x: Float, y: Float): Boolean {
-        return v.left <= x && v.right >= x && v.top <= y && v.bottom >= y
+    fun isValidMenuInput(ev: MotionEvent): Boolean {
+        val x = ev.rawX
+        val y = ev.rawY
+        return !viewsLaidOut() || (menuPosition.x <= x && menuPosition.x + menuWidth >= x &&
+                menuPosition.y <= y && menuPosition.y + menuHeight >= y)
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 6a9258c..afe837e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -279,11 +279,12 @@
         }
 
         outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
-        final int captionWidth = params.mCaptionWidthId != Resources.ID_NULL
+        outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL
                 ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
-        outResult.mCaptionX = (outResult.mWidth - captionWidth) / 2;
+        outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2;
 
-        startT.setWindowCrop(mCaptionContainerSurface, captionWidth, outResult.mCaptionHeight)
+        startT.setWindowCrop(mCaptionContainerSurface, outResult.mCaptionWidth,
+                        outResult.mCaptionHeight)
                 .setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */)
                 .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER)
                 .show(mCaptionContainerSurface);
@@ -356,7 +357,7 @@
         // Caption view
         mCaptionWindowManager.setConfiguration(taskConfig);
         final WindowManager.LayoutParams lp =
-                new WindowManager.LayoutParams(captionWidth, outResult.mCaptionHeight,
+                new WindowManager.LayoutParams(outResult.mCaptionWidth, outResult.mCaptionHeight,
                         WindowManager.LayoutParams.TYPE_APPLICATION,
                         WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
         lp.setTitle("Caption of Task=" + mTaskInfo.taskId);
@@ -578,6 +579,7 @@
 
     static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
         int mCaptionHeight;
+        int mCaptionWidth;
         int mCaptionX;
         int mWidth;
         int mHeight;
@@ -587,6 +589,7 @@
             mWidth = 0;
             mHeight = 0;
             mCaptionHeight = 0;
+            mCaptionWidth = 0;
             mCaptionX = 0;
             mRootView = null;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
index 5f77022..6dcae27 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
@@ -5,7 +5,6 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.res.ColorStateList
 import android.graphics.Color
-import android.graphics.PointF
 import android.view.View
 import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
 import android.widget.ImageButton
@@ -47,17 +46,6 @@
         animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
     }
 
-    /**
-     * Returns true if input point is in the caption's view.
-     * @param inputPoint the input point relative to the task in full "focus" (i.e. fullscreen).
-     */
-    fun pointInCaption(inputPoint: PointF, captionX: Int): Boolean {
-        return inputPoint.x >= captionX &&
-                inputPoint.x <= captionX + captionView.width &&
-                inputPoint.y >= 0 &&
-                inputPoint.y <= captionView.height
-    }
-
     private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
         return if (shouldUseLightCaptionColors(taskInfo)) {
             context.getColor(R.color.desktop_mode_caption_handle_bar_light)
diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/gui/Surface.h
index de1ba00..2573931 100644
--- a/libs/hostgraphics/gui/Surface.h
+++ b/libs/hostgraphics/gui/Surface.h
@@ -50,6 +50,8 @@
     virtual int unlockAndPost() { return 0; }
     virtual int query(int what, int* value) const { return 0; }
 
+    virtual void destroy() {}
+
 protected:
     virtual ~Surface() {}
 
diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp
index 16de21d..71f7926 100644
--- a/libs/hwui/HardwareBitmapUploader.cpp
+++ b/libs/hwui/HardwareBitmapUploader.cpp
@@ -379,7 +379,7 @@
         case kAlpha_8_SkColorType:
             formatInfo.isSupported = HardwareBitmapUploader::hasAlpha8Support();
             formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8_UNORM;
-            formatInfo.format = GL_R8;
+            formatInfo.format = GL_RED;
             formatInfo.type = GL_UNSIGNED_BYTE;
             formatInfo.vkFormat = VK_FORMAT_R8_UNORM;
             break;
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 58d9d8b..286f06a 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -578,16 +578,6 @@
         return result;
     }
 
-    // This method is kept for old Robolectric JNI signature used by SystemUIGoogleRoboRNGTests.
-    static jfloat getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric(
-            JNIEnv* env, jclass cls, jlong paintHandle, jcharArray text, jint start, jint end,
-            jint contextStart, jint contextEnd, jboolean isRtl, jint offset, jfloatArray advances,
-            jint advancesIndex, jobject drawBounds) {
-        return getRunCharacterAdvance___CIIIIZI_FI_F(env, cls, paintHandle, text, start, end,
-                                                     contextStart, contextEnd, isRtl, offset,
-                                                     advances, advancesIndex, drawBounds, nullptr);
-    }
-
     static jint doOffsetForAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[],
             jint start, jint count, jint bufSize, jboolean isRtl, jfloat advance) {
         minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
@@ -1163,8 +1153,6 @@
         {"nGetRunCharacterAdvance",
          "(J[CIIIIZI[FILandroid/graphics/RectF;Landroid/graphics/Paint$RunInfo;)F",
          (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F},
-        {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F",
-         (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric},
         {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I},
         {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
          (void*)PaintGlue::getFontMetricsIntForText___C},
diff --git a/media/java/android/media/tv/TvTrackInfo.java b/media/java/android/media/tv/TvTrackInfo.java
index 78d7d76..2ebb19a 100644
--- a/media/java/android/media/tv/TvTrackInfo.java
+++ b/media/java/android/media/tv/TvTrackInfo.java
@@ -55,6 +55,27 @@
      */
     public static final int TYPE_SUBTITLE = 2;
 
+    /**
+     * The component tag identifies a component carried by a MPEG-2 TS.
+     *
+     * This corresponds to the component_tag in the component descriptor in the
+     * Elementary Stream loop of the stream in the Program Map Table
+     * (PMT) [EN 300 468], or undefined if the component is not carried in an
+     * MPEG-2 TS.
+     *
+     * @hide
+     */
+    public static final String EXTRA_BUNDLE_KEY_COMPONENT_TAG = "component_tag";
+
+    /**
+     * The MPEG Program ID (PID) of the component in the MPEG2-TS in
+     * which it is carried, or undefined if the component is not carried in an
+     * MPEG-2 TS.
+     *
+     * @hide
+     */
+    public static final String EXTRA_BUNDLE_KEY_PID = "pid";
+
     private final int mType;
     private final String mId;
     private final String mLanguage;
diff --git a/media/java/android/media/tv/ad/ITvAdClient.aidl b/media/java/android/media/tv/ad/ITvAdClient.aidl
new file mode 100644
index 0000000..34d96b3
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdClient.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+import android.view.InputChannel;
+
+/**
+ * Interface a client of the ITvAdManager implements, to identify itself and receive
+ * information about changes to the state of each TV AD service.
+ * @hide
+ */
+oneway interface ITvAdClient {
+    void onSessionCreated(in String serviceId, IBinder token, in InputChannel channel, int seq);
+    void onSessionReleased(int seq);
+    void onLayoutSurface(int left, int top, int right, int bottom, int seq);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl
index 92cc923..a747e49 100644
--- a/media/java/android/media/tv/ad/ITvAdManager.aidl
+++ b/media/java/android/media/tv/ad/ITvAdManager.aidl
@@ -16,10 +16,25 @@
 
 package android.media.tv.ad;
 
+import android.media.tv.ad.ITvAdClient;
+import android.media.tv.ad.ITvAdManagerCallback;
+import android.media.tv.ad.TvAdServiceInfo;
+import android.view.Surface;
+
 /**
  * Interface to the TV AD service.
  * @hide
  */
 interface ITvAdManager {
+    List<TvAdServiceInfo> getTvAdServiceList(int userId);
+    void createSession(
+            in ITvAdClient client, in String serviceId, in String type, int seq, int userId);
+    void releaseSession(in IBinder sessionToken, int userId);
     void startAdService(in IBinder sessionToken, int userId);
+    void setSurface(in IBinder sessionToken, in Surface surface, int userId);
+    void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height,
+            int userId);
+
+    void registerCallback(in ITvAdManagerCallback callback, int userId);
+    void unregisterCallback(in ITvAdManagerCallback callback, int userId);
 }
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl
similarity index 67%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to media/java/android/media/tv/ad/ITvAdManagerCallback.aidl
index ce92b6d..f55f67e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl
@@ -13,10 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.media.tv.ad;
 
 /**
- * The configuration of a single virtual camera stream.
+ * Interface to receive callbacks from ITvAdManager regardless of sessions.
  * @hide
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+oneway interface ITvAdManagerCallback {
+    void onAdServiceAdded(in String serviceId);
+    void onAdServiceRemoved(in String serviceId);
+    void onAdServiceUpdated(in String serviceId);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdService.aidl b/media/java/android/media/tv/ad/ITvAdService.aidl
new file mode 100644
index 0000000..3bb0409
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdService.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+import android.media.tv.ad.ITvAdServiceCallback;
+import android.media.tv.ad.ITvAdSessionCallback;
+import android.os.Bundle;
+import android.view.InputChannel;
+
+/**
+ * Top-level interface to a TV AD component (implemented in a Service). It's used for
+ * TvAdManagerService to communicate with TvAdService.
+ * @hide
+ */
+oneway interface ITvAdService {
+    void registerCallback(in ITvAdServiceCallback callback);
+    void unregisterCallback(in ITvAdServiceCallback callback);
+    void createSession(in InputChannel channel, in ITvAdSessionCallback callback,
+            in String serviceId, in String type);
+    void sendAppLinkCommand(in Bundle command);
+}
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl
similarity index 78%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to media/java/android/media/tv/ad/ITvAdServiceCallback.aidl
index ce92b6d..a087181 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl
@@ -13,10 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.media.tv.ad;
 
 /**
- * The configuration of a single virtual camera stream.
+ * Helper interface for ITvAdService to allow the TvAdService to notify the TvAdManagerService.
  * @hide
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+oneway interface ITvAdServiceCallback {
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl
index b834f1b9..751257c 100644
--- a/media/java/android/media/tv/ad/ITvAdSession.aidl
+++ b/media/java/android/media/tv/ad/ITvAdSession.aidl
@@ -16,10 +16,15 @@
 
 package android.media.tv.ad;
 
+import android.view.Surface;
+
 /**
- * Sub-interface of ITvAdService which is created per session and has its own context.
+ * Sub-interface of ITvAdService.aidl which is created per session and has its own context.
  * @hide
  */
 oneway interface ITvAdSession {
+    void release();
     void startAdService();
+    void setSurface(in Surface surface);
+    void dispatchSurfaceChanged(int format, int width, int height);
 }
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
similarity index 63%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
index ce92b6d..f21ef19 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
@@ -13,10 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
+
+package android.media.tv.ad;
+
+import android.media.tv.ad.ITvAdSession;
 
 /**
- * The configuration of a single virtual camera stream.
+ * Helper interface for ITvAdSession to allow TvAdService to notify the system service when there is
+ * a related event.
  * @hide
  */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+oneway interface ITvAdSessionCallback {
+    void onSessionCreated(in ITvAdSession session);
+    void onLayoutSurface(int left, int top, int right, int bottom);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
new file mode 100644
index 0000000..4df2783
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+import android.content.Context;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.Surface;
+
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+
+/**
+ * Implements the internal ITvAdSession interface.
+ * @hide
+ */
+public class ITvAdSessionWrapper
+        extends ITvAdSession.Stub implements HandlerCaller.Callback {
+
+    private static final String TAG = "ITvAdSessionWrapper";
+
+    private static final int EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS = 1000;
+    private static final int EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS = 5 * 1000;
+    private static final int DO_RELEASE = 1;
+    private static final int DO_SET_SURFACE = 2;
+    private static final int DO_DISPATCH_SURFACE_CHANGED = 3;
+
+    private final HandlerCaller mCaller;
+    private TvAdService.Session mSessionImpl;
+    private InputChannel mChannel;
+    private TvAdEventReceiver mReceiver;
+
+    public ITvAdSessionWrapper(
+            Context context, TvAdService.Session mSessionImpl, InputChannel channel) {
+        this.mSessionImpl = mSessionImpl;
+        mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */);
+        mChannel = channel;
+        if (channel != null) {
+            mReceiver = new TvAdEventReceiver(channel, context.getMainLooper());
+        }
+    }
+
+    @Override
+    public void release() {
+        mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE));
+    }
+
+
+    @Override
+    public void executeMessage(Message msg) {
+        if (mSessionImpl == null) {
+            return;
+        }
+
+        long startTime = System.nanoTime();
+        switch (msg.what) {
+            case DO_RELEASE: {
+                mSessionImpl.release();
+                mSessionImpl = null;
+                if (mReceiver != null) {
+                    mReceiver.dispose();
+                    mReceiver = null;
+                }
+                if (mChannel != null) {
+                    mChannel.dispose();
+                    mChannel = null;
+                }
+                break;
+            }
+            case DO_SET_SURFACE: {
+                mSessionImpl.setSurface((Surface) msg.obj);
+                break;
+            }
+            case DO_DISPATCH_SURFACE_CHANGED: {
+                SomeArgs args = (SomeArgs) msg.obj;
+                mSessionImpl.dispatchSurfaceChanged(
+                        (Integer) args.argi1, (Integer) args.argi2, (Integer) args.argi3);
+                args.recycle();
+                break;
+            }
+            default: {
+                Log.w(TAG, "Unhandled message code: " + msg.what);
+                break;
+            }
+        }
+        long durationMs = (System.nanoTime() - startTime) / (1000 * 1000);
+        if (durationMs > EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS) {
+            Log.w(TAG, "Handling message (" + msg.what + ") took too long time (duration="
+                    + durationMs + "ms)");
+            if (durationMs > EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS) {
+                // TODO: handle timeout
+            }
+        }
+
+    }
+
+    @Override
+    public void startAdService() throws RemoteException {
+
+    }
+
+    @Override
+    public void setSurface(Surface surface) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface));
+    }
+
+    @Override
+    public void dispatchSurfaceChanged(int format, int width, int height) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0));
+    }
+
+    private final class TvAdEventReceiver extends InputEventReceiver {
+        TvAdEventReceiver(InputChannel inputChannel, Looper looper) {
+            super(inputChannel, looper);
+        }
+
+        @Override
+        public void onInputEvent(InputEvent event) {
+            if (mSessionImpl == null) {
+                // The session has been finished.
+                finishInputEvent(event, false);
+                return;
+            }
+
+            int handled = mSessionImpl.dispatchInputEvent(event, this);
+            if (handled != TvAdManager.Session.DISPATCH_IN_PROGRESS) {
+                finishInputEvent(
+                        event, handled == TvAdManager.Session.DISPATCH_HANDLED);
+            }
+        }
+    }
+}
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 2b52c4b..9c75051 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -17,12 +17,30 @@
 package android.media.tv.ad;
 
 import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemService;
 import android.content.Context;
+import android.media.tv.TvInputManager;
 import android.media.tv.flags.Flags;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
 import android.os.RemoteException;
 import android.util.Log;
+import android.util.Pools;
+import android.util.SparseArray;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventSender;
+import android.view.Surface;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * Central system API to the overall client-side TV AD architecture, which arbitrates interaction
@@ -37,10 +55,163 @@
     private final ITvAdManager mService;
     private final int mUserId;
 
+    // A mapping from the sequence number of a session to its SessionCallbackRecord.
+    private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap =
+            new SparseArray<>();
+
+    // @GuardedBy("mLock")
+    private final List<TvAdServiceCallbackRecord> mCallbackRecords = new ArrayList<>();
+
+    // A sequence number for the next session to be created. Should be protected by a lock
+    // {@code mSessionCallbackRecordMap}.
+    private int mNextSeq;
+
+    private final Object mLock = new Object();
+    private final ITvAdClient mClient;
+
     /** @hide */
     public TvAdManager(ITvAdManager service, int userId) {
         mService = service;
         mUserId = userId;
+        mClient = new ITvAdClient.Stub() {
+            @Override
+            public void onSessionCreated(String serviceId, IBinder token, InputChannel channel,
+                    int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for " + token);
+                        return;
+                    }
+                    Session session = null;
+                    if (token != null) {
+                        session = new Session(token, channel, mService, mUserId, seq,
+                                mSessionCallbackRecordMap);
+                    } else {
+                        mSessionCallbackRecordMap.delete(seq);
+                    }
+                    record.postSessionCreated(session);
+                }
+            }
+
+            @Override
+            public void onSessionReleased(int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    mSessionCallbackRecordMap.delete(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq:" + seq);
+                        return;
+                    }
+                    record.mSession.releaseInternal();
+                    record.postSessionReleased();
+                }
+            }
+
+            @Override
+            public void onLayoutSurface(int left, int top, int right, int bottom, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postLayoutSurface(left, top, right, bottom);
+                }
+            }
+
+        };
+
+        ITvAdManagerCallback managerCallback =
+                new ITvAdManagerCallback.Stub() {
+                    @Override
+                    public void onAdServiceAdded(String serviceId) {
+                        synchronized (mLock) {
+                            for (TvAdServiceCallbackRecord record : mCallbackRecords) {
+                                record.postAdServiceAdded(serviceId);
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onAdServiceRemoved(String serviceId) {
+                        synchronized (mLock) {
+                            for (TvAdServiceCallbackRecord record : mCallbackRecords) {
+                                record.postAdServiceRemoved(serviceId);
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onAdServiceUpdated(String serviceId) {
+                        synchronized (mLock) {
+                            for (TvAdServiceCallbackRecord record : mCallbackRecords) {
+                                record.postAdServiceUpdated(serviceId);
+                            }
+                        }
+                    }
+                };
+        try {
+            if (mService != null) {
+                mService.registerCallback(managerCallback, mUserId);
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the complete list of TV AD service on the system.
+     *
+     * @return List of {@link TvAdServiceInfo} for each TV AD service that describes its meta
+     * information.
+     * @hide
+     */
+    @NonNull
+    public List<TvAdServiceInfo> getTvAdServiceList() {
+        try {
+            return mService.getTvAdServiceList(mUserId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Creates a {@link Session} for a given TV AD service.
+     *
+     * <p>The number of sessions that can be created at the same time is limited by the capability
+     * of the given AD service.
+     *
+     * @param serviceId The ID of the AD service.
+     * @param callback A callback used to receive the created session.
+     * @param handler A {@link Handler} that the session creation will be delivered to.
+     * @hide
+     */
+    public void createSession(
+            @NonNull String serviceId,
+            @NonNull String type,
+            @NonNull final TvAdManager.SessionCallback callback,
+            @NonNull Handler handler) {
+        createSessionInternal(serviceId, type, callback, handler);
+    }
+
+    private void createSessionInternal(String serviceId, String type,
+            TvAdManager.SessionCallback callback, Handler handler) {
+        Preconditions.checkNotNull(serviceId);
+        Preconditions.checkNotNull(type);
+        Preconditions.checkNotNull(callback);
+        Preconditions.checkNotNull(handler);
+        TvAdManager.SessionCallbackRecord
+                record = new TvAdManager.SessionCallbackRecord(callback, handler);
+        synchronized (mSessionCallbackRecordMap) {
+            int seq = mNextSeq++;
+            mSessionCallbackRecordMap.put(seq, record);
+            try {
+                mService.createSession(mClient, serviceId, type, seq, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     /**
@@ -48,14 +219,121 @@
      * @hide
      */
     public static final class Session {
-        private final IBinder mToken;
+        static final int DISPATCH_IN_PROGRESS = -1;
+        static final int DISPATCH_NOT_HANDLED = 0;
+        static final int DISPATCH_HANDLED = 1;
+
+        private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500;
         private final ITvAdManager mService;
         private final int mUserId;
+        private final int mSeq;
+        private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap;
 
-        private Session(IBinder token, ITvAdManager service, int userId) {
+        // For scheduling input event handling on the main thread. This also serves as a lock to
+        // protect pending input events and the input channel.
+        private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper());
+
+        private TvInputManager.Session mInputSession;
+        private final Pools.Pool<PendingEvent> mPendingEventPool = new Pools.SimplePool<>(20);
+        private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20);
+        private TvInputEventSender mSender;
+        private InputChannel mInputChannel;
+        private IBinder mToken;
+
+        private Session(IBinder token, InputChannel channel, ITvAdManager service, int userId,
+                int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
             mToken = token;
+            mInputChannel = channel;
             mService = service;
             mUserId = userId;
+            mSeq = seq;
+            mSessionCallbackRecordMap = sessionCallbackRecordMap;
+        }
+
+        /**
+         * Releases this session.
+         */
+        public void release() {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.releaseSession(mToken, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+
+            releaseInternal();
+        }
+
+        /**
+         * Sets the {@link android.view.Surface} for this session.
+         *
+         * @param surface A {@link android.view.Surface} used to render AD.
+         */
+        public void setSurface(Surface surface) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            // surface can be null.
+            try {
+                mService.setSurface(mToken, surface, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Notifies of any structural changes (format or size) of the surface passed in
+         * {@link #setSurface}.
+         *
+         * @param format The new PixelFormat of the surface.
+         * @param width The new width of the surface.
+         * @param height The new height of the surface.
+         */
+        public void dispatchSurfaceChanged(int format, int width, int height) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        private void flushPendingEventsLocked() {
+            mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT);
+
+            final int count = mPendingEvents.size();
+            for (int i = 0; i < count; i++) {
+                int seq = mPendingEvents.keyAt(i);
+                Message msg = mHandler.obtainMessage(
+                        InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0);
+                msg.setAsynchronous(true);
+                msg.sendToTarget();
+            }
+        }
+
+        private void releaseInternal() {
+            mToken = null;
+            synchronized (mHandler) {
+                if (mInputChannel != null) {
+                    if (mSender != null) {
+                        flushPendingEventsLocked();
+                        mSender.dispose();
+                        mSender = null;
+                    }
+                    mInputChannel.dispose();
+                    mInputChannel = null;
+                }
+            }
+            synchronized (mSessionCallbackRecordMap) {
+                mSessionCallbackRecordMap.delete(mSeq);
+            }
         }
 
         void startAdService() {
@@ -69,5 +347,324 @@
                 throw e.rethrowFromSystemServer();
             }
         }
+
+        private final class InputEventHandler extends Handler {
+            public static final int MSG_SEND_INPUT_EVENT = 1;
+            public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
+            public static final int MSG_FLUSH_INPUT_EVENT = 3;
+
+            InputEventHandler(Looper looper) {
+                super(looper, null, true);
+            }
+
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MSG_SEND_INPUT_EVENT: {
+                        sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj);
+                        return;
+                    }
+                    case MSG_TIMEOUT_INPUT_EVENT: {
+                        finishedInputEvent(msg.arg1, false, true);
+                        return;
+                    }
+                    case MSG_FLUSH_INPUT_EVENT: {
+                        finishedInputEvent(msg.arg1, false, false);
+                        return;
+                    }
+                }
+            }
+        }
+
+        // Assumes the event has already been removed from the queue.
+        void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
+            p.mHandled = handled;
+            if (p.mEventHandler.getLooper().isCurrentThread()) {
+                // Already running on the callback handler thread so we can send the callback
+                // immediately.
+                p.run();
+            } else {
+                // Post the event to the callback handler thread.
+                // In this case, the callback will be responsible for recycling the event.
+                Message msg = Message.obtain(p.mEventHandler, p);
+                msg.setAsynchronous(true);
+                msg.sendToTarget();
+            }
+        }
+
+        // Must be called on the main looper
+        private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) {
+            synchronized (mHandler) {
+                int result = sendInputEventOnMainLooperLocked(p);
+                if (result == DISPATCH_IN_PROGRESS) {
+                    return;
+                }
+            }
+
+            invokeFinishedInputEventCallback(p, false);
+        }
+
+        private int sendInputEventOnMainLooperLocked(PendingEvent p) {
+            if (mInputChannel != null) {
+                if (mSender == null) {
+                    mSender = new TvInputEventSender(mInputChannel, mHandler.getLooper());
+                }
+
+                final InputEvent event = p.mEvent;
+                final int seq = event.getSequenceNumber();
+                if (mSender.sendInputEvent(seq, event)) {
+                    mPendingEvents.put(seq, p);
+                    Message msg = mHandler.obtainMessage(
+                            InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
+                    msg.setAsynchronous(true);
+                    mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT);
+                    return DISPATCH_IN_PROGRESS;
+                }
+
+                Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:"
+                        + event);
+            }
+            return DISPATCH_NOT_HANDLED;
+        }
+
+        void finishedInputEvent(int seq, boolean handled, boolean timeout) {
+            final PendingEvent p;
+            synchronized (mHandler) {
+                int index = mPendingEvents.indexOfKey(seq);
+                if (index < 0) {
+                    return; // spurious, event already finished or timed out
+                }
+
+                p = mPendingEvents.valueAt(index);
+                mPendingEvents.removeAt(index);
+
+                if (timeout) {
+                    Log.w(TAG, "Timeout waiting for session to handle input event after "
+                            + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
+                } else {
+                    mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
+                }
+            }
+
+            invokeFinishedInputEventCallback(p, handled);
+        }
+
+        private void recyclePendingEventLocked(PendingEvent p) {
+            p.recycle();
+            mPendingEventPool.release(p);
+        }
+
+        /**
+         * Callback that is invoked when an input event that was dispatched to this session has been
+         * finished.
+         *
+         * @hide
+         */
+        public interface FinishedInputEventCallback {
+            /**
+             * Called when the dispatched input event is finished.
+             *
+             * @param token A token passed to {@link #dispatchInputEvent}.
+             * @param handled {@code true} if the dispatched input event was handled properly.
+             *            {@code false} otherwise.
+             */
+            void onFinishedInputEvent(Object token, boolean handled);
+        }
+
+        private final class TvInputEventSender extends InputEventSender {
+            TvInputEventSender(InputChannel inputChannel, Looper looper) {
+                super(inputChannel, looper);
+            }
+
+            @Override
+            public void onInputEventFinished(int seq, boolean handled) {
+                finishedInputEvent(seq, handled, false);
+            }
+        }
+
+        private final class PendingEvent implements Runnable {
+            public InputEvent mEvent;
+            public Object mEventToken;
+            public Session.FinishedInputEventCallback mCallback;
+            public Handler mEventHandler;
+            public boolean mHandled;
+
+            public void recycle() {
+                mEvent = null;
+                mEventToken = null;
+                mCallback = null;
+                mEventHandler = null;
+                mHandled = false;
+            }
+
+            @Override
+            public void run() {
+                mCallback.onFinishedInputEvent(mEventToken, mHandled);
+
+                synchronized (mEventHandler) {
+                    recyclePendingEventLocked(this);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Interface used to receive the created session.
+     * @hide
+     */
+    public abstract static class SessionCallback {
+        /**
+         * This is called after {@link TvAdManager#createSession} has been processed.
+         *
+         * @param session A {@link TvAdManager.Session} instance created. This can be
+         *                {@code null} if the creation request failed.
+         */
+        public void onSessionCreated(@Nullable Session session) {
+        }
+
+        /**
+         * This is called when {@link TvAdManager.Session} is released.
+         * This typically happens when the process hosting the session has crashed or been killed.
+         *
+         * @param session the {@link TvAdManager.Session} instance released.
+         */
+        public void onSessionReleased(@NonNull Session session) {
+        }
+
+        /**
+         * This is called when {@link TvAdService.Session#layoutSurface} is called to
+         * change the layout of surface.
+         *
+         * @param session A {@link TvAdManager.Session} associated with this callback.
+         * @param left Left position.
+         * @param top Top position.
+         * @param right Right position.
+         * @param bottom Bottom position.
+         */
+        public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
+        }
+
+    }
+
+    /**
+     * Callback used to monitor status of the TV AD service.
+     * @hide
+     */
+    public abstract static class TvAdServiceCallback {
+        /**
+         * This is called when a TV AD service is added to the system.
+         *
+         * <p>Normally it happens when the user installs a new TV AD service package that implements
+         * {@link TvAdService} interface.
+         *
+         * @param serviceId The ID of the TV AD service.
+         */
+        public void onAdServiceAdded(@NonNull String serviceId) {
+        }
+
+        /**
+         * This is called when a TV AD service is removed from the system.
+         *
+         * <p>Normally it happens when the user uninstalls the previously installed TV AD service
+         * package.
+         *
+         * @param serviceId The ID of the TV AD service.
+         */
+        public void onAdServiceRemoved(@NonNull String serviceId) {
+        }
+
+        /**
+         * This is called when a TV AD service is updated on the system.
+         *
+         * <p>Normally it happens when a previously installed TV AD service package is re-installed
+         * or a newer version of the package exists becomes available/unavailable.
+         *
+         * @param serviceId The ID of the TV AD service.
+         */
+        public void onAdServiceUpdated(@NonNull String serviceId) {
+        }
+
+    }
+
+    private static final class SessionCallbackRecord {
+        private final SessionCallback mSessionCallback;
+        private final Handler mHandler;
+        private Session mSession;
+
+        SessionCallbackRecord(SessionCallback sessionCallback, Handler handler) {
+            mSessionCallback = sessionCallback;
+            mHandler = handler;
+        }
+
+        void postSessionCreated(final Session session) {
+            mSession = session;
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onSessionCreated(session);
+                }
+            });
+        }
+
+        void postSessionReleased() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onSessionReleased(mSession);
+                }
+            });
+        }
+
+        void postLayoutSurface(final int left, final int top, final int right,
+                final int bottom) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom);
+                }
+            });
+        }
+    }
+
+    private static final class TvAdServiceCallbackRecord {
+        private final TvAdServiceCallback mCallback;
+        private final Executor mExecutor;
+
+        TvAdServiceCallbackRecord(TvAdServiceCallback callback, Executor executor) {
+            mCallback = callback;
+            mExecutor = executor;
+        }
+
+        public TvAdServiceCallback getCallback() {
+            return mCallback;
+        }
+
+        public void postAdServiceAdded(final String serviceId) {
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onAdServiceAdded(serviceId);
+                }
+            });
+        }
+
+        public void postAdServiceRemoved(final String serviceId) {
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onAdServiceRemoved(serviceId);
+                }
+            });
+        }
+
+        public void postAdServiceUpdated(final String serviceId) {
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onAdServiceUpdated(serviceId);
+                }
+            });
+        }
     }
 }
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index 6897a78..6995703 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -16,8 +16,37 @@
 
 package android.media.tv.ad;
 
+import android.annotation.CallSuper;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
 import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.InputChannel;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.internal.os.SomeArgs;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * The TvAdService class represents a TV client-side advertisement service.
@@ -36,9 +65,123 @@
     public static final String SERVICE_META_DATA = "android.media.tv.ad.service";
 
     /**
+     * This is the interface name that a service implementing a TV AD service should
+     * say that it supports -- that is, this is the action it uses for its intent filter. To be
+     * supported, the service must also require the
+     * android.Manifest.permission#BIND_TV_AD_SERVICE permission so that other
+     * applications cannot abuse it.
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+    public static final String SERVICE_INTERFACE = "android.media.tv.ad.TvAdService";
+
+    private final Handler mServiceHandler = new ServiceHandler();
+    private final RemoteCallbackList<ITvAdServiceCallback> mCallbacks = new RemoteCallbackList<>();
+
+    @Override
+    @Nullable
+    public final IBinder onBind(@NonNull Intent intent) {
+        ITvAdService.Stub tvAdServiceBinder = new ITvAdService.Stub() {
+            @Override
+            public void registerCallback(ITvAdServiceCallback cb) {
+                if (cb != null) {
+                    mCallbacks.register(cb);
+                }
+            }
+
+            @Override
+            public void unregisterCallback(ITvAdServiceCallback cb) {
+                if (cb != null) {
+                    mCallbacks.unregister(cb);
+                }
+            }
+
+            @Override
+            public void createSession(InputChannel channel, ITvAdSessionCallback cb,
+                    String serviceId, String type) {
+                if (cb == null) {
+                    return;
+                }
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = channel;
+                args.arg2 = cb;
+                args.arg3 = serviceId;
+                args.arg4 = type;
+                mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args)
+                        .sendToTarget();
+            }
+
+            @Override
+            public void sendAppLinkCommand(Bundle command) {
+                onAppLinkCommand(command);
+            }
+        };
+        return tvAdServiceBinder;
+    }
+
+    /**
+     * Called when app link command is received.
+     */
+    public void onAppLinkCommand(@NonNull Bundle command) {
+    }
+
+
+    /**
+     * Returns a concrete implementation of {@link Session}.
+     *
+     * <p>May return {@code null} if this TV AD service fails to create a session for some
+     * reason.
+     *
+     * @param serviceId The ID of the TV AD associated with the session.
+     * @param type The type of the TV AD associated with the session.
+     */
+    @Nullable
+    public abstract Session onCreateSession(@NonNull String serviceId, @NonNull String type);
+
+    /**
      * Base class for derived classes to implement to provide a TV AD session.
      */
     public abstract static class Session implements KeyEvent.Callback {
+        private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
+
+        private final Object mLock = new Object();
+        // @GuardedBy("mLock")
+        private ITvAdSessionCallback mSessionCallback;
+        // @GuardedBy("mLock")
+        private final List<Runnable> mPendingActions = new ArrayList<>();
+        private final Context mContext;
+        final Handler mHandler;
+        private final WindowManager mWindowManager;
+        private Surface mSurface;
+
+
+        /**
+         * Creates a new Session.
+         *
+         * @param context The context of the application
+         */
+        public Session(@NonNull Context context) {
+            mContext = context;
+            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+            mHandler = new Handler(context.getMainLooper());
+        }
+
+        /**
+         * Releases TvAdService session.
+         */
+        public abstract void onRelease();
+
+        void release() {
+            onRelease();
+            if (mSurface != null) {
+                mSurface.release();
+                mSurface = null;
+            }
+            synchronized (mLock) {
+                mSessionCallback = null;
+                mPendingActions.clear();
+            }
+        }
+
         /**
          * Starts TvAdService session.
          */
@@ -48,21 +191,264 @@
         void startAdService() {
             onStartAdService();
         }
-    }
 
-    /**
-     * Implements the internal ITvAdService interface.
-     */
-    public static class ITvAdSessionWrapper extends ITvAdSession.Stub {
-        private final Session mSessionImpl;
-
-        public ITvAdSessionWrapper(Session mSessionImpl) {
-            this.mSessionImpl = mSessionImpl;
+        @Override
+        public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+            return false;
         }
 
         @Override
-        public void startAdService() {
-            mSessionImpl.startAdService();
+        public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
+            return false;
         }
+
+        @Override
+        public boolean onKeyMultiple(int keyCode, int count, @NonNull KeyEvent event) {
+            return false;
+        }
+
+        @Override
+        public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
+            return false;
+        }
+
+        /**
+         * Implement this method to handle touch screen motion events on the current session.
+         *
+         * @param event The motion event being received.
+         * @return If you handled the event, return {@code true}. If you want to allow the event to
+         *         be handled by the next receiver, return {@code false}.
+         * @see View#onTouchEvent
+         */
+        public boolean onTouchEvent(@NonNull MotionEvent event) {
+            return false;
+        }
+
+        /**
+         * Implement this method to handle trackball events on the current session.
+         *
+         * @param event The motion event being received.
+         * @return If you handled the event, return {@code true}. If you want to allow the event to
+         *         be handled by the next receiver, return {@code false}.
+         * @see View#onTrackballEvent
+         */
+        public boolean onTrackballEvent(@NonNull MotionEvent event) {
+            return false;
+        }
+
+        /**
+         * Implement this method to handle generic motion events on the current session.
+         *
+         * @param event The motion event being received.
+         * @return If you handled the event, return {@code true}. If you want to allow the event to
+         *         be handled by the next receiver, return {@code false}.
+         * @see View#onGenericMotionEvent
+         */
+        public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
+            return false;
+        }
+
+        /**
+         * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position
+         * is relative to the overlay view that sits on top of this surface.
+         *
+         * @param left Left position in pixels, relative to the overlay view.
+         * @param top Top position in pixels, relative to the overlay view.
+         * @param right Right position in pixels, relative to the overlay view.
+         * @param bottom Bottom position in pixels, relative to the overlay view.
+         */
+        @CallSuper
+        public void layoutSurface(final int left, final int top, final int right,
+                final int bottom) {
+            if (left > right || top > bottom) {
+                throw new IllegalArgumentException("Invalid parameter");
+            }
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top
+                                    + ", r=" + right + ", b=" + bottom + ",)");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onLayoutSurface(left, top, right, bottom);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in layoutSurface", e);
+                    }
+                }
+            });
+        }
+
+        /**
+         * Called when the application sets the surface.
+         *
+         * <p>The TV AD service should render AD UI onto the given surface. When called with
+         * {@code null}, the AD service should immediately free any references to the currently set
+         * surface and stop using it.
+         *
+         * @param surface The surface to be used for AD UI rendering. Can be {@code null}.
+         * @return {@code true} if the surface was set successfully, {@code false} otherwise.
+         */
+        public abstract boolean onSetSurface(@Nullable Surface surface);
+
+        /**
+         * Called after any structural changes (format or size) have been made to the surface passed
+         * in {@link #onSetSurface}. This method is always called at least once, after
+         * {@link #onSetSurface} is called with non-null surface.
+         *
+         * @param format The new {@link PixelFormat} of the surface.
+         * @param width The new width of the surface.
+         * @param height The new height of the surface.
+         */
+        public void onSurfaceChanged(@PixelFormat.Format int format, int width, int height) {
+        }
+
+        /**
+         * Takes care of dispatching incoming input events and tells whether the event was handled.
+         */
+        int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
+            if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")");
+            if (event instanceof KeyEvent) {
+                KeyEvent keyEvent = (KeyEvent) event;
+                if (keyEvent.dispatch(this, mDispatcherState, this)) {
+                    return TvAdManager.Session.DISPATCH_HANDLED;
+                }
+
+                // TODO: special handlings of navigation keys and media keys
+            } else if (event instanceof MotionEvent) {
+                MotionEvent motionEvent = (MotionEvent) event;
+                final int source = motionEvent.getSource();
+                if (motionEvent.isTouchEvent()) {
+                    if (onTouchEvent(motionEvent)) {
+                        return TvAdManager.Session.DISPATCH_HANDLED;
+                    }
+                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+                    if (onTrackballEvent(motionEvent)) {
+                        return TvAdManager.Session.DISPATCH_HANDLED;
+                    }
+                } else {
+                    if (onGenericMotionEvent(motionEvent)) {
+                        return TvAdManager.Session.DISPATCH_HANDLED;
+                    }
+                }
+            }
+            // TODO: handle overlay view
+            return TvAdManager.Session.DISPATCH_NOT_HANDLED;
+        }
+
+
+        private void initialize(ITvAdSessionCallback callback) {
+            synchronized (mLock) {
+                mSessionCallback = callback;
+                for (Runnable runnable : mPendingActions) {
+                    runnable.run();
+                }
+                mPendingActions.clear();
+            }
+        }
+
+        /**
+         * Calls {@link #onSetSurface}.
+         */
+        void setSurface(Surface surface) {
+            onSetSurface(surface);
+            if (mSurface != null) {
+                mSurface.release();
+            }
+            mSurface = surface;
+            // TODO: Handle failure.
+        }
+
+        /**
+         * Calls {@link #onSurfaceChanged}.
+         */
+        void dispatchSurfaceChanged(int format, int width, int height) {
+            if (DEBUG) {
+                Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width
+                        + ", height=" + height + ")");
+            }
+            onSurfaceChanged(format, width, height);
+        }
+
+        private void executeOrPostRunnableOnMainThread(Runnable action) {
+            synchronized (mLock) {
+                if (mSessionCallback == null) {
+                    // The session is not initialized yet.
+                    mPendingActions.add(action);
+                } else {
+                    if (mHandler.getLooper().isCurrentThread()) {
+                        action.run();
+                    } else {
+                        // Posts the runnable if this is not called from the main thread
+                        mHandler.post(action);
+                    }
+                }
+            }
+        }
+    }
+
+
+    @SuppressLint("HandlerLeak")
+    private final class ServiceHandler extends Handler {
+        private static final int DO_CREATE_SESSION = 1;
+        private static final int DO_NOTIFY_SESSION_CREATED = 2;
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case DO_CREATE_SESSION: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    InputChannel channel = (InputChannel) args.arg1;
+                    ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg2;
+                    String serviceId = (String) args.arg3;
+                    String type = (String) args.arg4;
+                    args.recycle();
+                    TvAdService.Session sessionImpl = onCreateSession(serviceId, type);
+                    if (sessionImpl == null) {
+                        try {
+                            // Failed to create a session.
+                            cb.onSessionCreated(null);
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "error in onSessionCreated", e);
+                        }
+                        return;
+                    }
+                    ITvAdSession stub =
+                            new ITvAdSessionWrapper(TvAdService.this, sessionImpl, channel);
+
+                    SomeArgs someArgs = SomeArgs.obtain();
+                    someArgs.arg1 = sessionImpl;
+                    someArgs.arg2 = stub;
+                    someArgs.arg3 = cb;
+                    mServiceHandler.obtainMessage(
+                            DO_NOTIFY_SESSION_CREATED, someArgs).sendToTarget();
+                    return;
+                }
+                case DO_NOTIFY_SESSION_CREATED: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    Session sessionImpl = (Session) args.arg1;
+                    ITvAdSession stub = (ITvAdSession) args.arg2;
+                    ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg3;
+                    try {
+                        cb.onSessionCreated(stub);
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "error in onSessionCreated", e);
+                    }
+                    if (sessionImpl != null) {
+                        sessionImpl.initialize(cb);
+                    }
+                    args.recycle();
+                    return;
+                }
+                default: {
+                    Log.w(TAG, "Unhandled message code: " + msg.what);
+                    return;
+                }
+            }
+        }
+
     }
 }
diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.java b/media/java/android/media/tv/ad/TvAdServiceInfo.java
index ed04f1f..45dc89d 100644
--- a/media/java/android/media/tv/ad/TvAdServiceInfo.java
+++ b/media/java/android/media/tv/ad/TvAdServiceInfo.java
@@ -24,6 +24,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -63,8 +64,7 @@
         if (context == null) {
             throw new IllegalArgumentException("context cannot be null.");
         }
-        // TODO: use a constant
-        Intent intent = new Intent("android.media.tv.ad.TvAdService").setComponent(component);
+        Intent intent = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component);
         ResolveInfo resolveInfo = context.getPackageManager().resolveService(
                 intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
         if (resolveInfo == null) {
@@ -80,6 +80,7 @@
 
         mService = resolveInfo;
         mId = id;
+        mTypes.addAll(types);
     }
 
     private TvAdServiceInfo(ResolveInfo service, String id, List<String> types) {
@@ -147,9 +148,8 @@
             ResolveInfo resolveInfo, Context context, List<String> types) {
         ServiceInfo serviceInfo = resolveInfo.serviceInfo;
         PackageManager pm = context.getPackageManager();
-        // TODO: use constant for the metadata
         try (XmlResourceParser parser =
-                     serviceInfo.loadXmlMetaData(pm, "android.media.tv.ad.service")) {
+                     serviceInfo.loadXmlMetaData(pm, TvAdService.SERVICE_META_DATA)) {
             if (parser == null) {
                 throw new IllegalStateException(
                         "No " + "android.media.tv.ad.service"
@@ -171,7 +171,15 @@
                         + XML_START_TAG_NAME + " tag for " + serviceInfo.name);
             }
 
-            // TODO: parse attributes
+            TypedArray sa = resources.obtainAttributes(attrs,
+                    com.android.internal.R.styleable.TvAdService);
+            CharSequence[] textArr = sa.getTextArray(
+                    com.android.internal.R.styleable.TvAdService_adServiceTypes);
+            for (CharSequence cs : textArr) {
+                types.add(cs.toString().toLowerCase());
+            }
+
+            sa.recycle();
         } catch (IOException | XmlPullParserException e) {
             throw new IllegalStateException(
                     "Failed reading meta-data for " + serviceInfo.packageName, e);
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index 1a3771a..5e67fe9 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -16,8 +16,20 @@
 
 package android.media.tv.ad;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.util.AttributeSet;
 import android.util.Log;
+import android.util.Xml;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
 import android.view.ViewGroup;
 
 /**
@@ -28,18 +40,166 @@
     private static final String TAG = "TvAdView";
     private static final boolean DEBUG = false;
 
-    // TODO: create session
-    private TvAdManager.Session mSession;
+    private final TvAdManager mTvAdManager;
 
-    public TvAdView(Context context) {
-        super(context, /* attrs = */null, /* defStyleAttr = */0);
+    private final Handler mHandler = new Handler();
+    private TvAdManager.Session mSession;
+    private MySessionCallback mSessionCallback;
+
+    private final AttributeSet mAttrs;
+    private final int mDefStyleAttr;
+    private final XmlResourceParser mParser;
+
+    private SurfaceView mSurfaceView;
+    private Surface mSurface;
+
+    private boolean mSurfaceChanged;
+    private int mSurfaceFormat;
+    private int mSurfaceWidth;
+    private int mSurfaceHeight;
+
+    private boolean mUseRequestedSurfaceLayout;
+    private int mSurfaceViewLeft;
+    private int mSurfaceViewRight;
+    private int mSurfaceViewTop;
+    private int mSurfaceViewBottom;
+
+
+
+    private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+            if (DEBUG) {
+                Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format
+                        + ", width=" + width + ", height=" + height + ")");
+            }
+            mSurfaceFormat = format;
+            mSurfaceWidth = width;
+            mSurfaceHeight = height;
+            mSurfaceChanged = true;
+            dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
+        }
+
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+            mSurface = holder.getSurface();
+            setSessionSurface(mSurface);
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            mSurface = null;
+            mSurfaceChanged = false;
+            setSessionSurface(null);
+        }
+    };
+
+
+    public TvAdView(@NonNull Context context) {
+        this(context, null, 0);
+    }
+
+    public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        int sourceResId = Resources.getAttributeSetSourceResId(attrs);
+        if (sourceResId != Resources.ID_NULL) {
+            Log.d(TAG, "Build local AttributeSet");
+            mParser  = context.getResources().getXml(sourceResId);
+            mAttrs = Xml.asAttributeSet(mParser);
+        } else {
+            Log.d(TAG, "Use passed in AttributeSet");
+            mParser = null;
+            mAttrs = attrs;
+        }
+        mDefStyleAttr = defStyleAttr;
+        resetSurfaceView();
+        mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE);
     }
 
     @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+    public void onLayout(boolean changed, int left, int top, int right, int bottom) {
         if (DEBUG) {
-            Log.d(TAG,
-                    "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)");
+            Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right
+                    + ", bottom=" + bottom + ",)");
+        }
+        if (mUseRequestedSurfaceLayout) {
+            mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight,
+                    mSurfaceViewBottom);
+        } else {
+            mSurfaceView.layout(0, 0, right - left, bottom - top);
+        }
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec);
+        int width = mSurfaceView.getMeasuredWidth();
+        int height = mSurfaceView.getMeasuredHeight();
+        int childState = mSurfaceView.getMeasuredState();
+        setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
+                resolveSizeAndState(height, heightMeasureSpec,
+                        childState << MEASURED_HEIGHT_STATE_SHIFT));
+    }
+
+    @Override
+    public void onVisibilityChanged(@NonNull View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        mSurfaceView.setVisibility(visibility);
+    }
+
+    private void resetSurfaceView() {
+        if (mSurfaceView != null) {
+            mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
+            removeView(mSurfaceView);
+        }
+        mSurface = null;
+        mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) {
+            @Override
+            protected void updateSurface() {
+                super.updateSurface();
+            }};
+        // The surface view's content should be treated as secure all the time.
+        mSurfaceView.setSecure(true);
+        mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
+        mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+
+        mSurfaceView.setZOrderOnTop(false);
+        mSurfaceView.setZOrderMediaOverlay(true);
+
+        addView(mSurfaceView);
+    }
+
+    private void setSessionSurface(Surface surface) {
+        if (mSession == null) {
+            return;
+        }
+        mSession.setSurface(surface);
+    }
+
+    private void dispatchSurfaceChanged(int format, int width, int height) {
+        if (mSession == null) {
+            return;
+        }
+        //mSession.dispatchSurfaceChanged(format, width, height);
+    }
+
+    /**
+     * Prepares the AD service of corresponding {@link TvAdService}.
+     *
+     * @param serviceId the AD service ID, which can be found in TvAdServiceInfo#getId().
+     */
+    public void prepareAdService(@NonNull String serviceId, @NonNull String type) {
+        if (DEBUG) {
+            Log.d(TAG, "prepareAdService");
+        }
+        mSessionCallback = new TvAdView.MySessionCallback(serviceId);
+        if (mTvAdManager != null) {
+            mTvAdManager.createSession(serviceId, type, mSessionCallback, mHandler);
         }
     }
 
@@ -54,4 +214,75 @@
             mSession.startAdService();
         }
     }
+
+    private class MySessionCallback extends TvAdManager.SessionCallback {
+        final String mServiceId;
+
+        MySessionCallback(String serviceId) {
+            mServiceId = serviceId;
+        }
+
+        @Override
+        public void onSessionCreated(TvAdManager.Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onSessionCreated()");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onSessionCreated - session already created");
+                // This callback is obsolete.
+                if (session != null) {
+                    session.release();
+                }
+                return;
+            }
+            mSession = session;
+            if (session != null) {
+                // mSurface may not be ready yet as soon as starting an application.
+                // In the case, we don't send Session.setSurface(null) unnecessarily.
+                // setSessionSurface will be called in surfaceCreated.
+                if (mSurface != null) {
+                    setSessionSurface(mSurface);
+                    if (mSurfaceChanged) {
+                        dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
+                    }
+                }
+            } else {
+                // Failed to create
+                // Todo: forward error to Tv App
+                mSessionCallback = null;
+            }
+        }
+
+        @Override
+        public void onSessionReleased(TvAdManager.Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onSessionReleased()");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onSessionReleased - session not created");
+                return;
+            }
+            mSessionCallback = null;
+            mSession = null;
+        }
+
+        @Override
+        public void onLayoutSurface(
+                TvAdManager.Session session, int left, int top, int right, int bottom) {
+            if (DEBUG) {
+                Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right="
+                        + right + ", bottom=" + bottom + ",)");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onLayoutSurface - session not created");
+                return;
+            }
+            mSurfaceViewLeft = left;
+            mSurfaceViewTop = top;
+            mSurfaceViewRight = right;
+            mSurfaceViewBottom = bottom;
+            mUseRequestedSurfaceLayout = true;
+            requestLayout();
+        }
+    }
 }
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 7739184..e3dba03 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -48,6 +48,7 @@
     void onRequestCurrentChannelLcn(int seq);
     void onRequestStreamVolume(int seq);
     void onRequestTrackInfoList(int seq);
+    void onRequestSelectedTrackInfo(int seq);
     void onRequestCurrentTvInputId(int seq);
     void onRequestTimeShiftMode(int seq);
     void onRequestAvailableSpeeds(int seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 41cbe4a..4316d05 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -102,6 +102,8 @@
             int UserId);
     void notifyAdResponse(in IBinder sessionToken, in AdResponse response, int UserId);
     void notifyAdBufferConsumed(in IBinder sessionToken, in AdBuffer buffer, int userId);
+    void sendSelectedTrackInfo(in IBinder sessionToken, in List<TvTrackInfo> tracks,
+            int userId);
 
     void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame,
             int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 052bc3d..ba7cf13 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -78,6 +78,7 @@
     void notifyBroadcastInfoResponse(in BroadcastInfoResponse response);
     void notifyAdResponse(in AdResponse response);
     void notifyAdBufferConsumed(in AdBuffer buffer);
+    void sendSelectedTrackInfo(in List<TvTrackInfo> tracks);
 
     void createMediaView(in IBinder windowToken, in Rect frame);
     void relayoutMediaView(in Rect frame);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index 9e43e79..416b8f1 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -50,6 +50,7 @@
     void onRequestCurrentTvInputId();
     void onRequestTimeShiftMode();
     void onRequestAvailableSpeeds();
+    void onRequestSelectedTrackInfo();
     void onRequestStartRecording(in String requestId, in Uri programUri);
     void onRequestStopRecording(in String recordingId);
     void onRequestScheduleRecording(in String requestId, in String inputId, in Uri channelUri,
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index 253ade8..518b08a 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -102,6 +102,7 @@
     private static final int DO_NOTIFY_RECORDING_SCHEDULED = 45;
     private static final int DO_SEND_TIME_SHIFT_MODE = 46;
     private static final int DO_SEND_AVAILABLE_SPEEDS = 47;
+    private static final int DO_SEND_SELECTED_TRACK_INFO = 48;
 
     private final HandlerCaller mCaller;
     private Session mSessionImpl;
@@ -247,6 +248,10 @@
                 args.recycle();
                 break;
             }
+            case DO_SEND_SELECTED_TRACK_INFO: {
+                mSessionImpl.sendSelectedTrackInfo((List<TvTrackInfo>) msg.obj);
+                break;
+            }
             case DO_NOTIFY_VIDEO_AVAILABLE: {
                 mSessionImpl.notifyVideoAvailable();
                 break;
@@ -526,6 +531,12 @@
     }
 
     @Override
+    public void sendSelectedTrackInfo(List<TvTrackInfo> tracks) {
+        mCaller.executeOrSendMessage(
+                mCaller.obtainMessageO(DO_SEND_SELECTED_TRACK_INFO, tracks));
+    }
+
+    @Override
     public void notifyTracksChanged(List<TvTrackInfo> tracks) {
         mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_TRACKS_CHANGED, tracks));
     }
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 7cce84a..bf4379f 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -33,7 +33,6 @@
 import android.media.tv.TvInputManager;
 import android.media.tv.TvRecordingInfo;
 import android.media.tv.TvTrackInfo;
-import android.media.tv.interactive.TvInteractiveAppService.Session;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -506,6 +505,18 @@
             }
 
             @Override
+            public void onRequestSelectedTrackInfo(int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestSelectedTrackInfo();
+                }
+            }
+
+            @Override
             public void onRequestCurrentTvInputId(int seq) {
                 synchronized (mSessionCallbackRecordMap) {
                     SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -1209,6 +1220,18 @@
             }
         }
 
+        void sendSelectedTrackInfo(@NonNull List<TvTrackInfo> tracks) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.sendSelectedTrackInfo(mToken, tracks, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
         void sendCurrentTvInputId(@Nullable String inputId) {
             if (mToken == null) {
                 Log.w(TAG, "The session has been already released");
@@ -2108,6 +2131,15 @@
             });
         }
 
+        void postRequestSelectedTrackInfo() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestSelectedTrackInfo(mSession);
+                }
+            });
+        }
+
         void postRequestCurrentTvInputId() {
             mHandler.post(new Runnable() {
                 @Override
@@ -2378,6 +2410,15 @@
         }
 
         /**
+         * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is
+         * called.
+         *
+         * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
+         */
+        public void onRequestSelectedTrackInfo(Session session) {
+        }
+
+        /**
          * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId} is
          * called.
          *
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 2419404..5cc86ba 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -932,6 +932,16 @@
                 @NonNull Bundle data) {
         }
 
+        /**
+         * Called when the TV App sends the selected track info as a response to
+         * requestSelectedTrackInfo.
+         *
+         * @param tracks
+         * @hide
+         */
+        public void onSelectedTrackInfo(List<TvTrackInfo> tracks) {
+        }
+
         @Override
         public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
             return false;
@@ -1338,6 +1348,30 @@
         }
 
         /**
+         * Requests the currently selected {@link TvTrackInfo} from the TV App.
+         *
+         * <p> Normally, track info cannot be synchronized until the channel has
+         * been changed. This is used when the session of the TIAS is newly
+         * created and the normal synchronization has not happened yet.
+         * @hide
+         */
+        @CallSuper
+        public void requestSelectedTrackInfo() {
+            executeOrPostRunnableOnMainThread(() -> {
+                try {
+                    if (DEBUG) {
+                        Log.d(TAG, "requestSelectedTrackInfo");
+                    }
+                    if (mSessionCallback != null) {
+                        mSessionCallback.onRequestSelectedTrackInfo();
+                    }
+                } catch (RemoteException e) {
+                    Log.w(TAG, "error in requestSelectedTrackInfo", e);
+                }
+            });
+        }
+
+        /**
          * Requests starting of recording
          *
          * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to
@@ -1781,6 +1815,13 @@
             onTvMessage(type, data);
         }
 
+        void sendSelectedTrackInfo(List<TvTrackInfo> tracks) {
+            if (DEBUG) {
+                Log.d(TAG, "notifySelectedTrackInfo (tracks= " + tracks + ")");
+            }
+            onSelectedTrackInfo(tracks);
+        }
+
         /**
          * Calls {@link #onAdBufferConsumed}.
          */
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index cbaf5e4..40a12e4 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -582,6 +582,20 @@
     }
 
     /**
+     * Sends the currently selected track info to the TV Interactive App.
+     *
+     * @hide
+     */
+    public void sendSelectedTrackInfo(@Nullable List<TvTrackInfo> tracks) {
+        if (DEBUG) {
+            Log.d(TAG, "sendSelectedTrackInfo");
+        }
+        if (mSession != null) {
+            mSession.sendSelectedTrackInfo(tracks);
+        }
+    }
+
+    /**
      * Sends current TV input ID to related TV interactive app.
      *
      * @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is
@@ -1197,6 +1211,16 @@
         }
 
         /**
+         * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is
+         * called.
+         *
+         * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+         * @hide
+         */
+        public void onRequestSelectedTrackInfo(@NonNull String iAppServiceId) {
+        }
+
+        /**
          * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId()} is
          * called.
          *
@@ -1714,6 +1738,28 @@
         }
 
         @Override
+        public void onRequestSelectedTrackInfo(Session session) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestSelectedTrackInfo");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestSelectedTrackInfo - session not created");
+                return;
+            }
+            synchronized (mCallbackLock) {
+                if (mCallbackExecutor != null) {
+                    mCallbackExecutor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (mCallback != null) {
+                                mCallback.onRequestSelectedTrackInfo(mIAppServiceId);
+                            }
+                        }
+                    });
+                }
+            }
+        }
+
+        @Override
         public void onRequestCurrentTvInputId(Session session) {
             if (DEBUG) {
                 Log.d(TAG, "onRequestCurrentTvInputId");
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 3fcb871..00b0e57 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -967,7 +967,7 @@
     ScopedLocalRef<jobject> filter(env);
     {
         android::Mutex::Autolock autoLock(mLock);
-        if (env->IsSameObject(filter.get(), nullptr)) {
+        if (env->IsSameObject(mFilterObj, nullptr)) {
             ALOGE("FilterClientCallbackImpl::onFilterStatus:"
                   "Filter object has been freed. Ignoring callback.");
             return;
diff --git a/nfc/Android.bp b/nfc/Android.bp
index 5d1404a..87da299 100644
--- a/nfc/Android.bp
+++ b/nfc/Android.bp
@@ -70,6 +70,10 @@
     lint: {
         strict_updatability_linting: true,
     },
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.nfcservices",
+    ],
 }
 
 filegroup {
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 75f5491..8219d2f 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -812,8 +812,8 @@
         if (context == null) {
             throw new IllegalArgumentException("context cannot be null");
         }
-        Context applicationContext = context.getApplicationContext();
-        if (applicationContext == null) {
+        context = context.getApplicationContext();
+        if (context == null) {
             throw new IllegalArgumentException(
                     "context not associated with any application (using a mock context?)");
         }
diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp
index 991fe41..c292b502 100644
--- a/packages/CredentialManager/Android.bp
+++ b/packages/CredentialManager/Android.bp
@@ -7,19 +7,14 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-android_app {
-    name: "CredentialManager",
-    defaults: ["platform_app_defaults"],
-    certificate: "platform",
+android_library {
+    name: "CredentialManager-handheld",
+
+    platform_apis: true,
+
     srcs: ["src/**/*.kt"],
     resource_dirs: ["res"],
 
-    dex_preopt: {
-        profile_guided: true,
-        //TODO: b/312357299 - Update baseline profile
-        profile: "profile.txt.prof",
-    },
-
     static_libs: [
         "CredentialManagerShared",
         "PlatformComposeCore",
@@ -42,6 +37,23 @@
         "androidx.recyclerview_recyclerview",
         "kotlinx-coroutines-core",
     ],
+}
+
+android_app {
+    name: "CredentialManager",
+    defaults: ["platform_app_defaults"],
+    certificate: "platform",
+
+    dex_preopt: {
+        profile_guided: true,
+        //TODO: b/312357299 - Update baseline profile
+        profile: "profile.txt.prof",
+    },
+
+    // Do not add new dependencies here. Add to CredentialManager-handheld instead.
+    static_libs: [
+        "CredentialManager-handheld",
+    ],
 
     platform_apis: true,
     privileged: true,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index c409ba6..f8ffc9e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -34,6 +34,7 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.res.stringResource
 import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.compose.theme.PlatformTheme
 import com.android.credentialmanager.common.Constants
 import com.android.credentialmanager.common.DialogState
 import com.android.credentialmanager.common.ProviderActivityResult
@@ -43,7 +44,6 @@
 import com.android.credentialmanager.createflow.hasContentToDisplay
 import com.android.credentialmanager.getflow.GetCredentialScreen
 import com.android.credentialmanager.getflow.hasContentToDisplay
-import com.android.credentialmanager.ui.theme.PlatformTheme
 
 @ExperimentalMaterialApi
 class CredentialSelectorActivity : ComponentActivity() {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 58467af..03ac605 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -173,7 +173,7 @@
                 CancellationSignal(),
                 Executors.newSingleThreadExecutor(),
                 outcome,
-                autofillCallback
+                autofillCallback.asBinder()
         )
     }
 
@@ -358,8 +358,8 @@
                 } else {
                     spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
                 }
-                val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY
-                        && primaryEntry.displayName != null) {
+                val displayName: String = if (primaryEntry.credentialType ==
+                        CredentialType.PASSKEY && primaryEntry.displayName != null) {
                     primaryEntry.displayName!!
                 } else {
                     primaryEntry.userName
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
index db69b8b..d319e4c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
@@ -24,11 +24,11 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import com.android.compose.rememberSystemUiController
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.common.material.ModalBottomSheetLayout
 import com.android.credentialmanager.common.material.ModalBottomSheetValue
 import com.android.credentialmanager.common.material.rememberModalBottomSheetState
 import com.android.credentialmanager.ui.theme.EntryShape
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 import kotlinx.coroutines.launch
 
 
@@ -54,7 +54,7 @@
         setBottomSheetSystemBarsColor(sysUiController)
     }
     ModalBottomSheetLayout(
-        sheetBackgroundColor = LocalAndroidColorScheme.current.colorSurfaceBright,
+        sheetBackgroundColor = LocalAndroidColorScheme.current.surfaceBright,
         modifier = Modifier.background(Color.Transparent),
         sheetState = state,
         sheetContent = sheetContent,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
index 3976f9a..bdfe399 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
@@ -30,8 +30,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.ui.theme.Shapes
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 
 /**
  * Container card for the whole sheet.
@@ -50,7 +50,7 @@
         modifier = modifier.fillMaxWidth().wrapContentHeight(),
         border = null,
         colors = CardDefaults.cardColors(
-            containerColor = LocalAndroidColorScheme.current.colorSurfaceBright,
+            containerColor = LocalAndroidColorScheme.current.surfaceBright,
         ),
     ) {
         if (topAppBar != null) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
index 1c394ec..a6253b8 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
@@ -56,9 +56,9 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.R
 import com.android.credentialmanager.ui.theme.EntryShape
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.ui.theme.Shapes
 
 @Composable
@@ -168,7 +168,7 @@
                             // Decorative purpose only.
                             contentDescription = null,
                             modifier = Modifier.size(24.dp),
-                            tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                            tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                         )
                     }
                 }
@@ -182,7 +182,7 @@
                         Icon(
                             modifier = iconSize,
                             bitmap = iconImageBitmap,
-                            tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                            tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                             // Decorative purpose only.
                             contentDescription = null,
                         )
@@ -206,7 +206,7 @@
                     Icon(
                         modifier = iconSize,
                         imageVector = iconImageVector,
-                        tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                        tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                         // Decorative purpose only.
                         contentDescription = null,
                     )
@@ -218,7 +218,7 @@
                     Icon(
                         modifier = iconSize,
                         painter = iconPainter,
-                        tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                        tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                         // Decorative purpose only.
                         contentDescription = null,
                     )
@@ -229,9 +229,9 @@
         },
         border = null,
         colors = SuggestionChipDefaults.suggestionChipColors(
-            containerColor = LocalAndroidColorScheme.current.colorSurfaceContainerHigh,
-            labelColor = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
-            iconContentColor = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+            containerColor = LocalAndroidColorScheme.current.surfaceContainerHigh,
+            labelColor = LocalAndroidColorScheme.current.onSurfaceVariant,
+            iconContentColor = LocalAndroidColorScheme.current.onSurfaceVariant,
         ),
     )
 }
@@ -294,7 +294,7 @@
         Icon(
             modifier = Modifier.size(24.dp),
             painter = leadingIconPainter,
-            tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+            tint = LocalAndroidColorScheme.current.onSurfaceVariant,
             // Decorative purpose only.
             contentDescription = null,
         )
@@ -353,7 +353,7 @@
                             R.string.accessibility_back_arrow_button
                         ),
                         modifier = Modifier.size(24.dp).autoMirrored(),
-                        tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+                        tint = LocalAndroidColorScheme.current.onSurfaceVariant,
                     )
                 }
             }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
index 2df0c7a9..342af3b 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
@@ -24,20 +24,20 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
+import com.android.compose.theme.LocalAndroidColorScheme
 
 @Composable
 fun CredentialListSectionHeader(text: String, isFirstSection: Boolean) {
     InternalSectionHeader(
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+        color = LocalAndroidColorScheme.current.onSurfaceVariant,
         applyTopPadding = !isFirstSection
     )
 }
 
 @Composable
 fun MoreAboutPasskeySectionHeader(text: String) {
-    InternalSectionHeader(text, LocalAndroidColorScheme.current.colorOnSurface)
+    InternalSectionHeader(text, LocalAndroidColorScheme.current.onSurface)
 }
 
 @Composable
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
index a619523..b4075f1 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import com.android.compose.SystemUiController
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.common.material.ModalBottomSheetDefaults
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 
 @Composable
 fun setTransparentSystemBarsColor(sysUiController: SystemUiController) {
@@ -34,7 +34,7 @@
         darkIcons = false
     )
     sysUiController.setNavigationBarColor(
-        color = LocalAndroidColorScheme.current.colorSurfaceBright,
+        color = LocalAndroidColorScheme.current.surfaceBright,
         darkIcons = false
     )
 }
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
index 6b46636..9111e61 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
@@ -25,7 +25,7 @@
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
+import com.android.compose.theme.LocalAndroidColorScheme
 
 /**
  * The headline for a screen. E.g. "Create a passkey for X", "Choose a saved sign-in for X".
@@ -37,7 +37,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurface,
+        color = LocalAndroidColorScheme.current.onSurface,
         textAlign = TextAlign.Center,
         style = MaterialTheme.typography.headlineSmall,
     )
@@ -51,7 +51,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+        color = LocalAndroidColorScheme.current.onSurfaceVariant,
         style = MaterialTheme.typography.bodyMedium,
     )
 }
@@ -69,7 +69,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+        color = LocalAndroidColorScheme.current.onSurfaceVariant,
         style = MaterialTheme.typography.bodySmall,
         overflow = TextOverflow.Ellipsis,
         maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE,
@@ -85,7 +85,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurface,
+        color = LocalAndroidColorScheme.current.onSurface,
         style = MaterialTheme.typography.titleLarge,
     )
 }
@@ -103,7 +103,7 @@
     Text(
         modifier = modifier.wrapContentSize(),
         text = text,
-        color = LocalAndroidColorScheme.current.colorOnSurface,
+        color = LocalAndroidColorScheme.current.onSurface,
         style = MaterialTheme.typography.titleSmall,
         overflow = TextOverflow.Ellipsis,
         maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE,
@@ -159,7 +159,7 @@
         modifier = modifier.wrapContentSize(),
         text = text,
         textAlign = TextAlign.Center,
-        color = LocalAndroidColorScheme.current.colorOnSurfaceVariant,
+        color = LocalAndroidColorScheme.current.onSurfaceVariant,
         style = MaterialTheme.typography.labelLarge,
     )
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 14a9165..f261d1f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -46,6 +46,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.drawable.toBitmap
+import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.credentialmanager.CredentialSelectorViewModel
 import com.android.credentialmanager.R
 import com.android.credentialmanager.model.EntryInfo
@@ -70,7 +71,6 @@
 import com.android.credentialmanager.logging.CreateCredentialEvent
 import com.android.credentialmanager.model.creation.CreateOptionInfo
 import com.android.credentialmanager.model.creation.RemoteInfo
-import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 import com.android.internal.logging.UiEventLogger.UiEventEnum
 
 @Composable
@@ -460,7 +460,7 @@
             item {
                 Divider(
                     thickness = 1.dp,
-                    color = LocalAndroidColorScheme.current.colorOutlineVariant,
+                    color = LocalAndroidColorScheme.current.outlineVariant,
                     modifier = Modifier.padding(vertical = 16.dp)
                 )
             }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index a291f59..458a99a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -26,7 +26,7 @@
 import com.android.internal.util.Preconditions
 
 data class GetCredentialUiState(
-        val isRequestForAllOptions: Boolean,
+    val isRequestForAllOptions: Boolean,
     val providerInfoList: List<ProviderInfo>,
     val requestDisplayInfo: RequestDisplayInfo,
     val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
@@ -165,7 +165,7 @@
     )
 }
 
-private fun toActiveEntry(
+fun toActiveEntry(
     providerDisplayInfo: ProviderDisplayInfo,
 ): EntryInfo? {
     val sortedUserNameToCredentialEntryList =
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt
deleted file mode 100644
index a33904d..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme
-
-import android.annotation.ColorInt
-import android.content.Context
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.graphics.Color
-import com.android.internal.R
-
-/** File copied from PlatformComposeCore. */
-
-/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */
-val LocalAndroidColorScheme =
-    staticCompositionLocalOf<AndroidColorScheme> {
-        throw IllegalStateException(
-            "No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " +
-                "Composable surrounded by a PlatformTheme {}."
-        )
-    }
-
-/**
- * The Android color scheme.
- *
- * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future,
- * most of the colors in this class will be removed in favor of their M3 counterpart.
- */
-class AndroidColorScheme internal constructor(context: Context) {
-    val colorSurfaceBright = getColor(context, R.attr.materialColorSurfaceBright)
-    val colorSurfaceContainerHigh = getColor(context, R.attr.materialColorSurfaceContainerHigh)
-    val colorOutlineVariant = getColor(context, R.attr.materialColorOutlineVariant)
-    val colorOnSurface = getColor(context, R.attr.materialColorOnSurface)
-    val colorOnSurfaceVariant = getColor(context, R.attr.materialColorOnSurfaceVariant)
-
-    companion object {
-        fun getColor(context: Context, attr: Int): Color {
-            val ta = context.obtainStyledAttributes(intArrayOf(attr))
-            @ColorInt val color = ta.getColor(0, 0)
-            ta.recycle()
-            return Color(color)
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt
deleted file mode 100644
index c923845..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme
-
-import android.annotation.AttrRes
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-
-/** Read the [Color] from the given [attribute]. */
-@Composable
-@ReadOnlyComposable
-fun colorAttr(@AttrRes attribute: Int): Color {
-    return AndroidColorScheme.getColor(LocalContext.current, attribute)
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt
deleted file mode 100644
index 2f1ce68..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme
-
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
-import com.android.credentialmanager.ui.theme.typography.TypeScaleTokens
-import com.android.credentialmanager.ui.theme.typography.TypefaceNames
-import com.android.credentialmanager.ui.theme.typography.TypefaceTokens
-import com.android.credentialmanager.ui.theme.typography.TypographyTokens
-import com.android.credentialmanager.ui.theme.typography.platformTypography
-
-/** File copied from PlatformComposeCore. */
-
-/**
- * The Material 3 theme that should wrap all Platform Composables.
- *
- * TODO(b/280685309): Merge with the official SysUI platform theme.
- */
-@Composable
-fun PlatformTheme(
-    isDarkTheme: Boolean = isSystemInDarkTheme(),
-    content: @Composable () -> Unit,
-) {
-    val context = LocalContext.current
-
-    val colorScheme =
-        if (isDarkTheme) {
-            dynamicDarkColorScheme(context)
-        } else {
-            dynamicLightColorScheme(context)
-        }
-    val androidColorScheme = AndroidColorScheme(context)
-    val typefaceNames = remember(context) { TypefaceNames.get(context) }
-    val typography =
-        remember(typefaceNames) {
-            platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames))))
-        }
-
-    MaterialTheme(colorScheme, typography = typography) {
-        CompositionLocalProvider(
-            LocalAndroidColorScheme provides androidColorScheme,
-        ) {
-            content()
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt
deleted file mode 100644
index 984e4f1..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme.typography
-
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Typography
-
-/** File copied from PlatformComposeCore. */
-
-/**
- * The typography for Platform Compose code.
- *
- * Do not use directly and call [MaterialTheme.typography] instead to access the different text
- * styles.
- */
-internal fun platformTypography(typographyTokens: TypographyTokens): Typography {
-    return Typography(
-        displayLarge = typographyTokens.displayLarge,
-        displayMedium = typographyTokens.displayMedium,
-        displaySmall = typographyTokens.displaySmall,
-        headlineLarge = typographyTokens.headlineLarge,
-        headlineMedium = typographyTokens.headlineMedium,
-        headlineSmall = typographyTokens.headlineSmall,
-        titleLarge = typographyTokens.titleLarge,
-        titleMedium = typographyTokens.titleMedium,
-        titleSmall = typographyTokens.titleSmall,
-        bodyLarge = typographyTokens.bodyLarge,
-        bodyMedium = typographyTokens.bodyMedium,
-        bodySmall = typographyTokens.bodySmall,
-        labelLarge = typographyTokens.labelLarge,
-        labelMedium = typographyTokens.labelMedium,
-        labelSmall = typographyTokens.labelSmall,
-    )
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt
deleted file mode 100644
index b2dd207..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme.typography
-
-import androidx.compose.ui.unit.sp
-
-/** File copied from PlatformComposeCore. */
-internal class TypeScaleTokens(typefaceTokens: TypefaceTokens) {
-    val bodyLargeFont = typefaceTokens.plain
-    val bodyLargeLineHeight = 24.0.sp
-    val bodyLargeSize = 16.sp
-    val bodyLargeTracking = 0.0.sp
-    val bodyLargeWeight = TypefaceTokens.WeightRegular
-    val bodyMediumFont = typefaceTokens.plain
-    val bodyMediumLineHeight = 20.0.sp
-    val bodyMediumSize = 14.sp
-    val bodyMediumTracking = 0.0.sp
-    val bodyMediumWeight = TypefaceTokens.WeightRegular
-    val bodySmallFont = typefaceTokens.plain
-    val bodySmallLineHeight = 16.0.sp
-    val bodySmallSize = 12.sp
-    val bodySmallTracking = 0.1.sp
-    val bodySmallWeight = TypefaceTokens.WeightRegular
-    val displayLargeFont = typefaceTokens.brand
-    val displayLargeLineHeight = 64.0.sp
-    val displayLargeSize = 57.sp
-    val displayLargeTracking = 0.0.sp
-    val displayLargeWeight = TypefaceTokens.WeightRegular
-    val displayMediumFont = typefaceTokens.brand
-    val displayMediumLineHeight = 52.0.sp
-    val displayMediumSize = 45.sp
-    val displayMediumTracking = 0.0.sp
-    val displayMediumWeight = TypefaceTokens.WeightRegular
-    val displaySmallFont = typefaceTokens.brand
-    val displaySmallLineHeight = 44.0.sp
-    val displaySmallSize = 36.sp
-    val displaySmallTracking = 0.0.sp
-    val displaySmallWeight = TypefaceTokens.WeightRegular
-    val headlineLargeFont = typefaceTokens.brand
-    val headlineLargeLineHeight = 40.0.sp
-    val headlineLargeSize = 32.sp
-    val headlineLargeTracking = 0.0.sp
-    val headlineLargeWeight = TypefaceTokens.WeightRegular
-    val headlineMediumFont = typefaceTokens.brand
-    val headlineMediumLineHeight = 36.0.sp
-    val headlineMediumSize = 28.sp
-    val headlineMediumTracking = 0.0.sp
-    val headlineMediumWeight = TypefaceTokens.WeightRegular
-    val headlineSmallFont = typefaceTokens.brand
-    val headlineSmallLineHeight = 32.0.sp
-    val headlineSmallSize = 24.sp
-    val headlineSmallTracking = 0.0.sp
-    val headlineSmallWeight = TypefaceTokens.WeightRegular
-    val labelLargeFont = typefaceTokens.plain
-    val labelLargeLineHeight = 20.0.sp
-    val labelLargeSize = 14.sp
-    val labelLargeTracking = 0.0.sp
-    val labelLargeWeight = TypefaceTokens.WeightMedium
-    val labelMediumFont = typefaceTokens.plain
-    val labelMediumLineHeight = 16.0.sp
-    val labelMediumSize = 12.sp
-    val labelMediumTracking = 0.1.sp
-    val labelMediumWeight = TypefaceTokens.WeightMedium
-    val labelSmallFont = typefaceTokens.plain
-    val labelSmallLineHeight = 16.0.sp
-    val labelSmallSize = 11.sp
-    val labelSmallTracking = 0.1.sp
-    val labelSmallWeight = TypefaceTokens.WeightMedium
-    val titleLargeFont = typefaceTokens.brand
-    val titleLargeLineHeight = 28.0.sp
-    val titleLargeSize = 22.sp
-    val titleLargeTracking = 0.0.sp
-    val titleLargeWeight = TypefaceTokens.WeightRegular
-    val titleMediumFont = typefaceTokens.plain
-    val titleMediumLineHeight = 24.0.sp
-    val titleMediumSize = 16.sp
-    val titleMediumTracking = 0.0.sp
-    val titleMediumWeight = TypefaceTokens.WeightMedium
-    val titleSmallFont = typefaceTokens.plain
-    val titleSmallLineHeight = 20.0.sp
-    val titleSmallSize = 14.sp
-    val titleSmallTracking = 0.0.sp
-    val titleSmallWeight = TypefaceTokens.WeightMedium
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt
deleted file mode 100644
index 3cc761f..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:OptIn(ExperimentalTextApi::class)
-
-package com.android.credentialmanager.ui.theme.typography
-
-import android.content.Context
-import androidx.compose.ui.text.ExperimentalTextApi
-import androidx.compose.ui.text.font.DeviceFontFamilyName
-import androidx.compose.ui.text.font.Font
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-
-/** File copied from PlatformComposeCore. */
-internal class TypefaceTokens(typefaceNames: TypefaceNames) {
-    companion object {
-        val WeightMedium = FontWeight.Medium
-        val WeightRegular = FontWeight.Normal
-    }
-
-    private val brandFont = DeviceFontFamilyName(typefaceNames.brand)
-    private val plainFont = DeviceFontFamilyName(typefaceNames.plain)
-
-    val brand =
-        FontFamily(
-            Font(brandFont, weight = WeightMedium),
-            Font(brandFont, weight = WeightRegular),
-        )
-    val plain =
-        FontFamily(
-            Font(plainFont, weight = WeightMedium),
-            Font(plainFont, weight = WeightRegular),
-        )
-}
-
-internal data class TypefaceNames
-private constructor(
-    val brand: String,
-    val plain: String,
-) {
-    private enum class Config(val configName: String, val default: String) {
-        Brand("config_headlineFontFamily", "sans-serif"),
-        Plain("config_bodyFontFamily", "sans-serif"),
-    }
-
-    companion object {
-        fun get(context: Context): TypefaceNames {
-            return TypefaceNames(
-                brand = getTypefaceName(context, Config.Brand),
-                plain = getTypefaceName(context, Config.Plain),
-            )
-        }
-
-        private fun getTypefaceName(context: Context, config: Config): String {
-            return context
-                .getString(context.resources.getIdentifier(config.configName, "string", "android"))
-                .takeIf { it.isNotEmpty() }
-                ?: config.default
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt
deleted file mode 100644
index aadab92..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.ui.theme.typography
-
-import androidx.compose.ui.text.TextStyle
-
-/** File copied from PlatformComposeCore. */
-internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
-    val bodyLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.bodyLargeFont,
-            fontWeight = typeScaleTokens.bodyLargeWeight,
-            fontSize = typeScaleTokens.bodyLargeSize,
-            lineHeight = typeScaleTokens.bodyLargeLineHeight,
-            letterSpacing = typeScaleTokens.bodyLargeTracking,
-        )
-    val bodyMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.bodyMediumFont,
-            fontWeight = typeScaleTokens.bodyMediumWeight,
-            fontSize = typeScaleTokens.bodyMediumSize,
-            lineHeight = typeScaleTokens.bodyMediumLineHeight,
-            letterSpacing = typeScaleTokens.bodyMediumTracking,
-        )
-    val bodySmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.bodySmallFont,
-            fontWeight = typeScaleTokens.bodySmallWeight,
-            fontSize = typeScaleTokens.bodySmallSize,
-            lineHeight = typeScaleTokens.bodySmallLineHeight,
-            letterSpacing = typeScaleTokens.bodySmallTracking,
-        )
-    val displayLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.displayLargeFont,
-            fontWeight = typeScaleTokens.displayLargeWeight,
-            fontSize = typeScaleTokens.displayLargeSize,
-            lineHeight = typeScaleTokens.displayLargeLineHeight,
-            letterSpacing = typeScaleTokens.displayLargeTracking,
-        )
-    val displayMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.displayMediumFont,
-            fontWeight = typeScaleTokens.displayMediumWeight,
-            fontSize = typeScaleTokens.displayMediumSize,
-            lineHeight = typeScaleTokens.displayMediumLineHeight,
-            letterSpacing = typeScaleTokens.displayMediumTracking,
-        )
-    val displaySmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.displaySmallFont,
-            fontWeight = typeScaleTokens.displaySmallWeight,
-            fontSize = typeScaleTokens.displaySmallSize,
-            lineHeight = typeScaleTokens.displaySmallLineHeight,
-            letterSpacing = typeScaleTokens.displaySmallTracking,
-        )
-    val headlineLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.headlineLargeFont,
-            fontWeight = typeScaleTokens.headlineLargeWeight,
-            fontSize = typeScaleTokens.headlineLargeSize,
-            lineHeight = typeScaleTokens.headlineLargeLineHeight,
-            letterSpacing = typeScaleTokens.headlineLargeTracking,
-        )
-    val headlineMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.headlineMediumFont,
-            fontWeight = typeScaleTokens.headlineMediumWeight,
-            fontSize = typeScaleTokens.headlineMediumSize,
-            lineHeight = typeScaleTokens.headlineMediumLineHeight,
-            letterSpacing = typeScaleTokens.headlineMediumTracking,
-        )
-    val headlineSmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.headlineSmallFont,
-            fontWeight = typeScaleTokens.headlineSmallWeight,
-            fontSize = typeScaleTokens.headlineSmallSize,
-            lineHeight = typeScaleTokens.headlineSmallLineHeight,
-            letterSpacing = typeScaleTokens.headlineSmallTracking,
-        )
-    val labelLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.labelLargeFont,
-            fontWeight = typeScaleTokens.labelLargeWeight,
-            fontSize = typeScaleTokens.labelLargeSize,
-            lineHeight = typeScaleTokens.labelLargeLineHeight,
-            letterSpacing = typeScaleTokens.labelLargeTracking,
-        )
-    val labelMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.labelMediumFont,
-            fontWeight = typeScaleTokens.labelMediumWeight,
-            fontSize = typeScaleTokens.labelMediumSize,
-            lineHeight = typeScaleTokens.labelMediumLineHeight,
-            letterSpacing = typeScaleTokens.labelMediumTracking,
-        )
-    val labelSmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.labelSmallFont,
-            fontWeight = typeScaleTokens.labelSmallWeight,
-            fontSize = typeScaleTokens.labelSmallSize,
-            lineHeight = typeScaleTokens.labelSmallLineHeight,
-            letterSpacing = typeScaleTokens.labelSmallTracking,
-        )
-    val titleLarge =
-        TextStyle(
-            fontFamily = typeScaleTokens.titleLargeFont,
-            fontWeight = typeScaleTokens.titleLargeWeight,
-            fontSize = typeScaleTokens.titleLargeSize,
-            lineHeight = typeScaleTokens.titleLargeLineHeight,
-            letterSpacing = typeScaleTokens.titleLargeTracking,
-        )
-    val titleMedium =
-        TextStyle(
-            fontFamily = typeScaleTokens.titleMediumFont,
-            fontWeight = typeScaleTokens.titleMediumWeight,
-            fontSize = typeScaleTokens.titleMediumSize,
-            lineHeight = typeScaleTokens.titleMediumLineHeight,
-            letterSpacing = typeScaleTokens.titleMediumTracking,
-        )
-    val titleSmall =
-        TextStyle(
-            fontFamily = typeScaleTokens.titleSmallFont,
-            fontWeight = typeScaleTokens.titleSmallWeight,
-            fontSize = typeScaleTokens.titleSmallSize,
-            lineHeight = typeScaleTokens.titleSmallLineHeight,
-            letterSpacing = typeScaleTokens.titleSmallTracking,
-        )
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
index b5af845..9af799c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
@@ -31,7 +31,7 @@
 import android.os.Process;
 import android.util.Log;
 
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -105,7 +105,7 @@
         }
     }
 
-    @Nullable
+    @NonNull
     private String[] getRequestedPermissions(String callingPackage) {
         String[] requestedPermissions = null;
         try {
@@ -115,7 +115,7 @@
             // Should be unreachable because we've just fetched the packageName above.
             Log.e(TAG, "Package not found for " + callingPackage);
         }
-        return requestedPermissions;
+        return requestedPermissions == null ? new String[]{} : requestedPermissions;
     }
 
     void startUnarchive() {
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml
index 0b27464..0764609 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml
@@ -66,6 +66,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:maxLines="2"
+            android:ellipsize="end"
             android:hyphenationFrequency="normalFast"
             android:lineBreakWordStyle="phrase"
             android:textAppearance="?android:attr/textAppearanceListItem"/>
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml
index 8bb56ff..4f1a910 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml
@@ -66,6 +66,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:maxLines="2"
+            android:ellipsize="end"
             android:textAppearance="?android:attr/textAppearanceListItem"/>
 
         <LinearLayout
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 965fdcf..ed90284 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -73,6 +73,11 @@
             android:authorities="com.android.spa.gallery.debug.provider"
             android:exported="false">
         </provider>
-
+        <activity
+            android:name="com.android.settingslib.spa.gallery.SpaDialogActivity"
+            android:excludeFromRecents="true"
+            android:exported="true"
+            android:theme="@style/Theme.SpaLib.Dialog">
+        </activity>
     </application>
 </manifest>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt
new file mode 100644
index 0000000..8b80fe2
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.settingslib.spa.gallery
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.WarningAmber
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.android.settingslib.spa.framework.common.LogCategory
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.dialog.getDialogWidth
+
+
+class SpaDialogActivity : ComponentActivity() {
+    private val spaEnvironment get() = SpaEnvironmentFactory.instance
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK)
+        setContent {
+            SettingsTheme {
+                Content()
+            }
+        }
+    }
+
+    @Composable
+    fun Content() {
+        var openAlertDialog by remember { mutableStateOf(false) }
+        AlertDialog(openAlertDialog)
+        LaunchedEffect(key1 = Unit) {
+            openAlertDialog = true
+        }
+    }
+
+    @Composable
+    fun AlertDialog(openAlertDialog: Boolean) {
+        when {
+            openAlertDialog -> {
+                AlertDialogExample(
+                    onDismissRequest = { finish() },
+                    onConfirmation = { finish() },
+                    dialogTitle = intent.getStringExtra(DIALOG_TITLE) ?: DIALOG_TITLE,
+                    dialogText = intent.getStringExtra(DIALOG_TEXT) ?: DIALOG_TEXT,
+                    icon = Icons.Default.WarningAmber
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun AlertDialogExample(
+        onDismissRequest: () -> Unit,
+        onConfirmation: () -> Unit,
+        dialogTitle: String,
+        dialogText: String,
+        icon: ImageVector,
+    ) {
+        AlertDialog(
+            modifier = Modifier.width(getDialogWidth()),
+            icon = {
+                Icon(icon, contentDescription = null)
+            },
+            title = {
+                Text(text = dialogTitle)
+            },
+            text = {
+                Text(text = dialogText)
+            },
+            onDismissRequest = {
+                onDismissRequest()
+            },
+            dismissButton = {
+                OutlinedButton(
+                    onClick = {
+                        onDismissRequest()
+                    }
+                ) {
+                    Text(intent.getStringExtra(DISMISS_TEXT) ?: DISMISS_TEXT)
+                }
+            },
+            confirmButton = {
+                Button(
+                    onClick = {
+                        onConfirmation()
+                    },
+                ) {
+                    Text(intent.getStringExtra(CONFIRM_TEXT) ?: CONFIRM_TEXT)
+                }
+            }
+        )
+    }
+
+    companion object {
+        private const val TAG = "SpaDialogActivity"
+        private const val DIALOG_TITLE = "dialogTitle"
+        private const val DIALOG_TEXT = "dialogText"
+        private const val CONFIRM_TEXT = "confirmText"
+        private const val DISMISS_TEXT = "dismissText"
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml
index 25846ec..4b5a9bc 100644
--- a/packages/SettingsLib/Spa/spa/res/values/themes.xml
+++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml
@@ -22,4 +22,6 @@
         <item name="android:windowActionBar">false</item>
         <item name="android:windowNoTitle">true</item>
     </style>
+
+    <style name="Theme.SpaLib.Dialog" parent="Theme.Material3.DayNight.Dialog"/>
 </resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
index 207c174..8ffd799 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
@@ -99,7 +99,7 @@
 }
 
 @Composable
-private fun getDialogWidth(): Dp {
+fun getDialogWidth(): Dp {
     val configuration = LocalConfiguration.current
     return configuration.screenWidthDp.dp * when (configuration.orientation) {
         Configuration.ORIENTATION_LANDSCAPE -> 0.6f
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 5aa2bfc..a4b3af9 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -338,6 +338,9 @@
     <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=5165842622743212268] -->
     <string name="bluetooth_talkback_input_peripheral">Input Peripheral</string>
 
+    <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=26580326066627664] -->
+    <string name="bluetooth_talkback_hearing_aids">Hearing Aids</string>
+
     <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=5615463912185280812] -->
     <string name="bluetooth_talkback_bluetooth">Bluetooth</string>
 
@@ -1079,6 +1082,10 @@
 
     <!-- [CHAR_LIMIT=NONE] Label for battery on main page of settings -->
     <string name="power_remaining_settings_home_page"><xliff:g id="percentage" example="10%">%1$s</xliff:g> - <xliff:g id="time_string" example="1 hour left based on your usage">%2$s</xliff:g></string>
+    <!-- [CHAR_LIMIT=NONE] Label for charging on hold on main page of settings -->
+    <string name="power_charging_on_hold_settings_home_page"><xliff:g id="level">%1$s</xliff:g> - Charging on hold to protect battery</string>
+    <!-- [CHAR_LIMIT=NONE] Label for incompatible charging accessory on main page of settings -->
+    <string name="power_incompatible_charging_settings_home_page"><xliff:g id="level">%1$s</xliff:g> - Checking charging accessory</string>
     <!-- [CHAR_LIMIT=40] Label for estimated remaining duration of battery discharging -->
     <string name="power_remaining_duration_only">About <xliff:g id="time_remaining">%1$s</xliff:g> left</string>
     <!-- [CHAR_LIMIT=40] Label for battery level chart when discharging with duration -->
@@ -1139,11 +1146,13 @@
     <!-- Battery Info screen. Value for a status item.  Used for diagnostic info screens, precise translation isn't needed -->
     <string name="battery_info_status_discharging">Not charging</string>
     <!-- Battery Info screen. Value for a status item. A state which device is connected with any charger(e.g. USB, Adapter or Wireless) but not charging yet. Used for diagnostic info screens, precise translation isn't needed -->
-    <string name="battery_info_status_not_charging">Connected, not charging</string>
+    <string name="battery_info_status_not_charging">Connected, but not charging</string>
     <!-- Battery Info screen. Value for a status item.  Used for diagnostic info screens, precise translation isn't needed -->
     <string name="battery_info_status_full">Charged</string>
     <!-- [CHAR_LIMIT=40] Battery Info screen. Value for a status item. A state which device is fully charged -->
     <string name="battery_info_status_full_charged">Fully Charged</string>
+    <!-- [CHAR_LIMIT=None] Battery Info screen. Value for a status item. A state which device charging on hold -->
+    <string name="battery_info_status_charging_on_hold">Charging on hold</string>
 
     <!-- Summary for settings preference disabled by administrator [CHAR LIMIT=50] -->
     <string name="disabled_by_admin_summary_text">Controlled by admin</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index c2be571..fb14a17 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -1,6 +1,7 @@
 package com.android.settingslib;
 
 import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL;
+import static android.webkit.Flags.updateServiceV2;
 
 import android.annotation.ColorInt;
 import android.app.admin.DevicePolicyManager;
@@ -34,6 +35,7 @@
 import android.net.wifi.WifiInfo;
 import android.os.BatteryManager;
 import android.os.Build;
+import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -44,6 +46,9 @@
 import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
 import android.util.Log;
+import android.webkit.IWebViewUpdateService;
+import android.webkit.WebViewFactory;
+import android.webkit.WebViewProviderInfo;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -65,6 +70,8 @@
 
 public class Utils {
 
+    private static final String TAG = "Utils";
+
     @VisibleForTesting
     static final String STORAGE_MANAGER_ENABLED_PROPERTY =
             "ro.storage_manager.enabled";
@@ -76,6 +83,7 @@
     private static String sPermissionControllerPackageName;
     private static String sServicesSystemSharedLibPackageName;
     private static String sSharedSystemSharedLibPackageName;
+    private static String sDefaultWebViewPackageName;
 
     static final int[] WIFI_PIE = {
         com.android.internal.R.drawable.ic_wifi_signal_0,
@@ -445,6 +453,7 @@
                 || packageName.equals(sServicesSystemSharedLibPackageName)
                 || packageName.equals(sSharedSystemSharedLibPackageName)
                 || packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME)
+                || (updateServiceV2() && packageName.equals(getDefaultWebViewPackageName()))
                 || isDeviceProvisioningPackage(resources, packageName);
     }
 
@@ -459,6 +468,29 @@
     }
 
     /**
+     * Fetch the package name of the default WebView provider.
+     */
+    @Nullable
+    private static String getDefaultWebViewPackageName() {
+        if (sDefaultWebViewPackageName != null) {
+            return sDefaultWebViewPackageName;
+        }
+
+        try {
+            IWebViewUpdateService service = WebViewFactory.getUpdateService();
+            if (service != null) {
+                WebViewProviderInfo provider = service.getDefaultWebViewPackage();
+                if (provider != null) {
+                    sDefaultWebViewPackageName = provider.packageName;
+                }
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException when trying to fetch default WebView package Name", e);
+        }
+        return sDefaultWebViewPackageName;
+    }
+
+    /**
      * Returns the Wifi icon resource for a given RSSI level.
      *
      * @param level The number of bars to show (0-4)
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 0ffcc45..071afaf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -125,7 +125,8 @@
                 // The device should show hearing aid icon if it contains any hearing aid related
                 // profiles
                 if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
-                    return new Pair<>(getBluetoothDrawable(context, profileResId), null);
+                    return new Pair<>(getBluetoothDrawable(context, profileResId),
+                            context.getString(R.string.bluetooth_talkback_hearing_aids));
                 }
                 if (resId == 0) {
                     resId = profileResId;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 245fe6e..032a838 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -410,10 +410,14 @@
         connectDevice();
     }
 
-    void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
+    public void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
         mHearingAidInfo = hearingAidInfo;
     }
 
+    public HearingAidInfo getHearingAidInfo() {
+        return mHearingAidInfo;
+    }
+
     /**
      * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device
      */
@@ -1788,4 +1792,40 @@
     boolean getUnpairing() {
         return mUnpairing;
     }
+
+    ListenableFuture<Void> syncProfileForMemberDevice() {
+        return ThreadUtils.getBackgroundExecutor()
+            .submit(
+                () -> {
+                    List<Pair<LocalBluetoothProfile, Boolean>> toSync =
+                        Stream.of(
+                            mProfileManager.getA2dpProfile(),
+                            mProfileManager.getHeadsetProfile(),
+                            mProfileManager.getHearingAidProfile(),
+                            mProfileManager.getLeAudioProfile(),
+                            mProfileManager.getLeAudioBroadcastAssistantProfile())
+                        .filter(Objects::nonNull)
+                        .map(profile -> new Pair<>(profile, profile.isEnabled(mDevice)))
+                        .toList();
+
+                    for (var t : toSync) {
+                        LocalBluetoothProfile profile = t.first;
+                        boolean enabledForMain = t.second;
+
+                        for (var member : mMemberDevices) {
+                            BluetoothDevice btDevice = member.getDevice();
+
+                            if (enabledForMain != profile.isEnabled(btDevice)) {
+                                Log.d(TAG, "Syncing profile " + profile + " to "
+                                        + enabledForMain + " for member device "
+                                        + btDevice.getAnonymizedAddress() + " of main device "
+                                        + mDevice.getAnonymizedAddress());
+                                profile.setEnabled(btDevice, enabledForMain);
+                            }
+                        }
+                    }
+                    return null;
+                }
+            );
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index a6536a8c..89fe268 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -349,6 +349,7 @@
         if (profileId == BluetoothProfile.HEADSET
                 || profileId == BluetoothProfile.A2DP
                 || profileId == BluetoothProfile.LE_AUDIO
+                || profileId == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
                 || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
             return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
                 state);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index a49314a..e67ec48 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -379,6 +379,7 @@
         if (hasChanged) {
             log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
                     + mCachedDevices);
+            preferredMainDevice.syncProfileForMemberDevice();
         }
         return hasChanged;
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index ed545ab..9db8b47 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -18,7 +18,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -56,6 +58,8 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.shadow.api.Shadow;
 
+import java.util.concurrent.ExecutionException;
+
 @RunWith(RobolectricTestRunner.class)
 @Config(shadows = {ShadowBluetoothAdapter.class})
 public class CachedBluetoothDeviceTest {
@@ -1815,6 +1819,52 @@
         assertThat(mCachedDevice.isConnectedHearingAidDevice()).isFalse();
     }
 
+    @Test
+    public void syncProfileForMemberDevice_hasDiff_shouldSync()
+            throws ExecutionException, InterruptedException {
+        mCachedDevice.addMemberDevice(mSubCachedDevice);
+        when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+        when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+        when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+
+        when(mA2dpProfile.isEnabled(mDevice)).thenReturn(true);
+        when(mHearingAidProfile.isEnabled(mDevice)).thenReturn(true);
+        when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+
+        when(mA2dpProfile.isEnabled(mSubDevice)).thenReturn(true);
+        when(mHearingAidProfile.isEnabled(mSubDevice)).thenReturn(false);
+        when(mLeAudioProfile.isEnabled(mSubDevice)).thenReturn(false);
+
+        mCachedDevice.syncProfileForMemberDevice().get();
+
+        verify(mA2dpProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+        verify(mHearingAidProfile).setEnabled(any(BluetoothDevice.class), eq(true));
+        verify(mLeAudioProfile).setEnabled(any(BluetoothDevice.class), eq(true));
+    }
+
+    @Test
+    public void syncProfileForMemberDevice_noDiff_shouldNotSync()
+            throws ExecutionException, InterruptedException {
+        mCachedDevice.addMemberDevice(mSubCachedDevice);
+        when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+        when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+        when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+
+        when(mA2dpProfile.isEnabled(mDevice)).thenReturn(false);
+        when(mHearingAidProfile.isEnabled(mDevice)).thenReturn(false);
+        when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+
+        when(mA2dpProfile.isEnabled(mSubDevice)).thenReturn(false);
+        when(mHearingAidProfile.isEnabled(mSubDevice)).thenReturn(false);
+        when(mLeAudioProfile.isEnabled(mSubDevice)).thenReturn(true);
+
+        mCachedDevice.syncProfileForMemberDevice().get();
+
+        verify(mA2dpProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+        verify(mHearingAidProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+        verify(mLeAudioProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+    }
+
     private HearingAidInfo getLeftAshaHearingAidInfo() {
         return new HearingAidInfo.Builder()
                 .setAshaDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT)
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index b0abf92..2d442f4 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -1349,6 +1349,26 @@
             final int nameCount = names.size();
             HashMap<String, String> flagsToValues = new HashMap<>(names.size());
 
+            if (Flags.loadAconfigDefaults()) {
+                Map<String, Map<String, String>> allDefaults =
+                        settingsState.getAconfigDefaultValues();
+
+                if (allDefaults != null) {
+                    if (prefix != null) {
+                        String namespace = prefix.substring(0, prefix.length() - 1);
+
+                        Map<String, String> namespaceDefaults = allDefaults.get(namespace);
+                        if (namespaceDefaults != null) {
+                            flagsToValues.putAll(namespaceDefaults);
+                        }
+                    } else {
+                        for (Map<String, String> namespaceDefaults : allDefaults.values()) {
+                            flagsToValues.putAll(namespaceDefaults);
+                        }
+                    }
+                }
+            }
+
             for (int i = 0; i < nameCount; i++) {
                 String name = names.get(i);
                 Setting setting = settingsState.getSettingLocked(name);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 73c2e22..6f3c88f 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -69,6 +69,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -236,6 +237,10 @@
     @GuardedBy("mLock")
     private int mNextHistoricalOpIdx;
 
+    @GuardedBy("mLock")
+    @Nullable
+    private Map<String, Map<String, String>> mNamespaceDefaults;
+
     public static final int SETTINGS_TYPE_GLOBAL = 0;
     public static final int SETTINGS_TYPE_SYSTEM = 1;
     public static final int SETTINGS_TYPE_SECURE = 2;
@@ -331,25 +336,21 @@
             readStateSyncLocked();
 
             if (Flags.loadAconfigDefaults()) {
-                // Only load aconfig defaults if this is the first boot, the XML
-                // file doesn't exist yet, or this device is on its first boot after
-                // an OTA.
-                boolean shouldLoadAconfigValues = isConfigSettingsKey(mKey)
-                        && (!file.exists()
-                                || mContext.getPackageManager().isDeviceUpgrading());
-                if (shouldLoadAconfigValues) {
+                if (isConfigSettingsKey(mKey)) {
                     loadAconfigDefaultValuesLocked();
                 }
             }
+
         }
     }
 
     @GuardedBy("mLock")
     private void loadAconfigDefaultValuesLocked() {
+        mNamespaceDefaults = new HashMap<>();
+
         for (String fileName : sAconfigTextProtoFilesOnDevice) {
             try (FileInputStream inputStream = new FileInputStream(fileName)) {
-                byte[] contents = inputStream.readAllBytes();
-                loadAconfigDefaultValues(contents);
+                loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults);
             } catch (IOException e) {
                 Slog.e(LOG_TAG, "failed to read protobuf", e);
             }
@@ -358,27 +359,21 @@
 
     @VisibleForTesting
     @GuardedBy("mLock")
-    public void loadAconfigDefaultValues(byte[] fileContents) {
+    public static void loadAconfigDefaultValues(byte[] fileContents,
+            @NonNull Map<String, Map<String, String>> defaultMap) {
         try {
-            parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents);
-
-            if (parsedFlags == null) {
-                Slog.e(LOG_TAG, "failed to parse aconfig protobuf");
-                return;
-            }
-
+            parsed_flags parsedFlags =
+                    parsed_flags.parseFrom(fileContents);
             for (parsed_flag flag : parsedFlags.getParsedFlagList()) {
-                String flagName = flag.getNamespace() + "/"
-                        + flag.getPackage() + "." + flag.getName();
-                String value = flag.getState() == flag_state.ENABLED ? "true" : "false";
-
-                Setting existingSetting = getSettingLocked(flagName);
-                boolean isDefaultLoaded = existingSetting.getTag() != null
-                        && existingSetting.getTag().equals(BOOT_LOADED_DEFAULT_TAG);
-                if (existingSetting.getValue() == null || isDefaultLoaded) {
-                    insertSettingLocked(flagName, value, BOOT_LOADED_DEFAULT_TAG,
-                            false, flag.getPackage());
+                if (!defaultMap.containsKey(flag.getNamespace())) {
+                    Map<String, String> defaults = new HashMap<>();
+                    defaultMap.put(flag.getNamespace(), defaults);
                 }
+                String flagName = flag.getNamespace()
+                        + "/" + flag.getPackage() + "." + flag.getName();
+                String flagValue = flag.getState() == flag_state.ENABLED
+                        ? "true" : "false";
+                defaultMap.get(flag.getNamespace()).put(flagName, flagValue);
             }
         } catch (IOException e) {
             Slog.e(LOG_TAG, "failed to parse protobuf", e);
@@ -443,6 +438,13 @@
         return names;
     }
 
+    @Nullable
+    public Map<String, Map<String, String>> getAconfigDefaultValues() {
+        synchronized (mLock) {
+            return mNamespaceDefaults;
+        }
+    }
+
     // The settings provider must hold its lock when calling here.
     public Setting getSettingLocked(String name) {
         if (TextUtils.isEmpty(name)) {
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index d26c5ff..16de478 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -231,6 +231,7 @@
                     Settings.Global.ENABLE_ADB_INCREMENTAL_INSTALL_DEFAULT,
                     Settings.Global.ENABLE_MULTI_SLOT_TIMEOUT_MILLIS,
                     Settings.Global.ENHANCED_4G_MODE_ENABLED,
+                    Settings.Global.ENABLE_16K_PAGES, // Added for 16K developer option
                     Settings.Global.EPHEMERAL_COOKIE_MAX_SIZE_BYTES,
                     Settings.Global.ERROR_LOGCAT_PREFIX,
                     Settings.Global.EUICC_PROVISIONED,
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 24625ea..e55bbec 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -30,6 +30,8 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.PrintStream;
+import java.util.HashMap;
+import java.util.Map;
 
 public class SettingsStateTest extends AndroidTestCase {
     public static final String CRAZY_STRING =
@@ -93,7 +95,6 @@
         SettingsState settingsState = new SettingsState(
                 getContext(), lock, mSettingsFile, configKey,
                 SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
-
         parsed_flags flags = parsed_flags
                 .newBuilder()
                 .addParsedFlag(parsed_flag
@@ -117,18 +118,13 @@
                 .build();
 
         synchronized (lock) {
-            settingsState.loadAconfigDefaultValues(flags.toByteArray());
-            settingsState.persistSettingsLocked();
-        }
-        settingsState.waitForHandler();
+            Map<String, Map<String, String>> defaults = new HashMap<>();
+            settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
+            Map<String, String> namespaceDefaults = defaults.get("test_namespace");
+            assertEquals(2, namespaceDefaults.keySet().size());
 
-        synchronized (lock) {
-            assertEquals("false",
-                    settingsState.getSettingLocked(
-                        "test_namespace/com.android.flags.flag1").getValue());
-            assertEquals("true",
-                    settingsState.getSettingLocked(
-                        "test_namespace/com.android.flags.flag2").getValue());
+            assertEquals("false", namespaceDefaults.get("test_namespace/com.android.flags.flag1"));
+            assertEquals("true", namespaceDefaults.get("test_namespace/com.android.flags.flag2"));
         }
     }
 
@@ -150,21 +146,18 @@
                 .build();
 
         synchronized (lock) {
-            settingsState.loadAconfigDefaultValues(flags.toByteArray());
-            settingsState.persistSettingsLocked();
-        }
-        settingsState.waitForHandler();
+            Map<String, Map<String, String>> defaults = new HashMap<>();
+            settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
 
-        synchronized (lock) {
-            assertEquals(null,
-                    settingsState.getSettingLocked(
-                        "test_namespace/com.android.flags.flag1").getValue());
+            Map<String, String> namespaceDefaults = defaults.get("test_namespace");
+            assertEquals(null, namespaceDefaults);
         }
     }
 
     public void testInvalidAconfigProtoDoesNotCrash() {
+        Map<String, Map<String, String>> defaults = new HashMap<>();
         SettingsState settingsState = getSettingStateObject();
-        settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes());
+        settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes(), defaults);
     }
 
     public void testIsBinary() {
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
index 6c75b434..0df9bac 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -37,6 +37,7 @@
         "androidx.preference_preference",
         "androidx.viewpager_viewpager",
         "SettingsLibDisplayUtils",
+        "SettingsLibSettingsTheme",
         "com_android_a11y_menu_flags_lib",
     ],
 
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
index ca84265..648cc3b 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
@@ -40,7 +40,7 @@
             android:exported="true"
             android:label="@string/accessibility_menu_settings_name"
             android:launchMode="singleTop"
-            android:theme="@style/AccessibilityMenuSettings">
+            android:theme="@style/Theme.SettingsBase">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
 
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
index eadcd7c..f5db6a4 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
@@ -8,10 +8,3 @@
     description: "Hides the AccessibilityMenuService UI before taking action instead of after."
     bug: "292020123"
 }
-
-flag {
-    name: "a11y_menu_settings_back_button_fix_and_large_button_sizing"
-    namespace: "accessibility"
-    description: "Provides/restores back button functionality for the a11yMenu settings page. Also, fixes sizing problems with large shortcut buttons."
-    bug: "298467628"
-}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml
index 1f57654..81b3152 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml
@@ -16,10 +16,6 @@
 -->
 
 <resources>
-  <style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.DayNight">
-    <item name="android:windowLightStatusBar">false</item>
-  </style>
-
   <!--Adds the theme to support SnackBar component and user configurable theme. -->
   <style name="ServiceTheme" parent="android:Theme.DeviceDefault.DayNight">
     <item name="android:colorControlNormal">@color/colorControlNormal</item>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml
index a2508cd..4169155 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml
@@ -16,11 +16,6 @@
 -->
 
 <resources>
-  <!--The theme is for preference CollapsingToolbarBaseActivity settings-->
-  <style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.DayNight">
-    <item name="android:windowLightStatusBar">true</item>
-  </style>
-
   <!--Adds the theme to support SnackBar component and user configurable theme. -->
   <style name="ServiceTheme" parent="android:Theme.DeviceDefault.Light">
     <item name="android:colorControlNormal">@color/colorControlNormal</item>
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
index bf51e23..ab8f97a 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
@@ -35,7 +35,6 @@
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceManager;
 
-import com.android.systemui.accessibility.accessibilitymenu.Flags;
 import com.android.systemui.accessibility.accessibilitymenu.R;
 
 /**
@@ -56,28 +55,17 @@
 
         ActionBar actionBar = getActionBar();
         actionBar.setDisplayShowCustomEnabled(true);
-
-        if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) {
-            actionBar.setDisplayHomeAsUpEnabled(true);
-        }
+        actionBar.setDisplayHomeAsUpEnabled(true);
         actionBar.setCustomView(R.layout.preferences_action_bar);
         ((TextView) findViewById(R.id.action_bar_title)).setText(
                 getResources().getString(R.string.accessibility_menu_settings_name)
         );
-        actionBar.setDisplayOptions(
-                ActionBar.DISPLAY_TITLE_MULTIPLE_LINES
-                        | ActionBar.DISPLAY_SHOW_TITLE
-                        | ActionBar.DISPLAY_HOME_AS_UP);
     }
 
     @Override
     public boolean onNavigateUp() {
-        if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) {
-            mCallback.onBackInvoked();
-            return true;
-        } else {
-            return false;
-        }
+        mCallback.onBackInvoked();
+        return true;
     }
 
     /**
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
index c4f372c..c2cf6e1 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
@@ -28,7 +28,6 @@
 import android.widget.TextView;
 
 import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
-import com.android.systemui.accessibility.accessibilitymenu.Flags;
 import com.android.systemui.accessibility.accessibilitymenu.R;
 import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment;
 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
@@ -81,10 +80,8 @@
         if (convertView == null) {
             convertView = mInflater.inflate(R.layout.grid_item, parent, false);
 
-            if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) {
-                configureShortcutSize(convertView,
-                        A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService));
-            }
+            configureShortcutSize(convertView,
+                    A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService));
         }
 
         A11yMenuShortcut shortcutItem = (A11yMenuShortcut) getItem(position);
@@ -147,15 +144,6 @@
         ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
         TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);
 
-        if (!Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) {
-            if (A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService)) {
-                ViewGroup.LayoutParams params = shortcutIconButton.getLayoutParams();
-                params.width = (int) (params.width * LARGE_BUTTON_SCALE);
-                params.height = (int) (params.height * LARGE_BUTTON_SCALE);
-                shortcutLabel.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, mLargeTextSize);
-            }
-        }
-
         if (shortcutItem.getId() == A11yMenuShortcut.ShortcutId.UNSPECIFIED_ID_VALUE.ordinal()) {
             // Sets empty shortcut icon and label when the shortcut is ADD_ITEM.
             shortcutIconButton.setImageResource(android.R.color.transparent);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 5fbffbe..c23a49c 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -208,6 +208,13 @@
 }
 
 flag {
+    name: "compose_bouncer"
+    namespace: "systemui"
+    description: "Use the new compose bouncer in SystemUI"
+    bug: "310005730"
+}
+
+flag {
    name: "media_in_scene_container"
    namespace: "systemui"
    description: "Enable media in the scene container framework"
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 fd04b5ee0..f4ffb3c 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
@@ -22,6 +22,8 @@
 import android.view.WindowInsets
 import androidx.activity.ComponentActivity
 import androidx.lifecycle.LifecycleOwner
+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.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
@@ -85,6 +87,12 @@
         throwComposeUnavailableError()
     }
 
+    override fun createBouncer(
+        context: Context,
+        viewModel: BouncerViewModel,
+        dialogFactory: BouncerDialogFactory,
+    ): View = throwComposeUnavailableError()
+
     private fun throwComposeUnavailableError(): Nothing {
         error(
             "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
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 d31547b..43745f9 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
@@ -28,6 +28,9 @@
 import androidx.lifecycle.LifecycleOwner
 import com.android.compose.theme.PlatformTheme
 import com.android.compose.ui.platform.DensityAwareComposeView
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.composable.BouncerContent
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
 import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout
 import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider
@@ -171,4 +174,14 @@
     private fun Int.toDp(context: Context): Dp {
         return (this.toFloat() / context.resources.displayMetrics.density).dp
     }
+
+    override fun createBouncer(
+        context: Context,
+        viewModel: BouncerViewModel,
+        dialogFactory: BouncerDialogFactory,
+    ): View {
+        return ComposeView(context).apply {
+            setContent { PlatformTheme { BouncerContent(viewModel, dialogFactory) } }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt
index 1860c9f..2b1268e 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt
@@ -16,34 +16,14 @@
 
 package com.android.systemui.scene
 
-import android.app.AlertDialog
-import android.content.Context
-import com.android.systemui.bouncer.ui.composable.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.composable.BouncerScene
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.shared.model.Scene
-import com.android.systemui.statusbar.phone.SystemUIDialog
 import dagger.Binds
 import dagger.Module
-import dagger.Provides
 import dagger.multibindings.IntoSet
 
 @Module
 interface BouncerSceneModule {
 
     @Binds @IntoSet fun bouncerScene(scene: BouncerScene): Scene
-
-    companion object {
-
-        @Provides
-        @SysUISingleton
-        fun bouncerSceneDialogFactory(@Application context: Context): BouncerDialogFactory {
-            return object : BouncerDialogFactory {
-                override fun invoke(): AlertDialog {
-                    return SystemUIDialog(context)
-                }
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 6591543..d949396 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -81,6 +81,7 @@
 import com.android.compose.modifiers.thenIf
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
@@ -824,10 +825,6 @@
     }
 }
 
-interface BouncerDialogFactory {
-    operator fun invoke(): AlertDialog
-}
-
 /**
  * Calculates an alpha for the user switcher and bouncer such that it's at `1` when the offset of
  * the two reaches a stopping point but `0` in the middle of the transition.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index d638ffe..428bc39 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -24,6 +24,7 @@
 import androidx.compose.ui.Modifier
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.scene.shared.model.Direction
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 2fea6a5..b704864 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
@@ -1,27 +1,13 @@
 package com.android.systemui.communal.ui.compose
 
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
@@ -82,9 +68,7 @@
     // shade, which can be opened from many locations.
     val isKeyguardShowing by viewModel.isKeyguardVisible.collectAsState(initial = false)
 
-    // Failsafe to hide the whole SceneTransitionLayout in case of bugginess.
-    var showSceneTransitionLayout by remember { mutableStateOf(true) }
-    if (!showSceneTransitionLayout || !isKeyguardShowing) {
+    if (!isKeyguardShowing) {
         return
     }
 
@@ -109,7 +93,8 @@
                     Swipe(SwipeDirection.Left, fromEdge = Edge.Right) to TransitionSceneKey.Communal
                 )
         ) {
-            BlankScene { showSceneTransitionLayout = false }
+            // This scene shows nothing only allowing for transitions to the communal scene.
+            Box(modifier = Modifier.fillMaxSize())
         }
 
         scene(
@@ -124,31 +109,6 @@
     }
 }
 
-/**
- * Blank scene that shows over keyguard/dream. This scene will eventually show nothing at all and is
- * only used to allow for transitions to the communal scene.
- */
-@Composable
-private fun BlankScene(
-    modifier: Modifier = Modifier,
-    hideSceneTransitionLayout: () -> Unit,
-) {
-    Box(modifier.fillMaxSize()) {
-        Column(
-            Modifier.fillMaxHeight()
-                .width(ContainerDimensions.EdgeSwipeSize)
-                .align(Alignment.CenterEnd)
-                .background(Color(0x55e9f2eb)),
-            verticalArrangement = Arrangement.Center,
-            horizontalAlignment = Alignment.CenterHorizontally
-        ) {
-            IconButton(onClick = hideSceneTransitionLayout) {
-                Icon(Icons.Filled.Close, contentDescription = "Close button")
-            }
-        }
-    }
-}
-
 /** Scene containing the glanceable hub UI. */
 @Composable
 private fun SceneScope.CommunalScene(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index d76f0ff..91a4d2e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -88,8 +88,6 @@
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
-import com.android.systemui.media.controls.ui.MediaHierarchyManager
-import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.res.R
 
 @Composable
@@ -576,10 +574,6 @@
     AndroidView(
         modifier = modifier,
         factory = {
-            viewModel.mediaHost.expansion = MediaHostState.EXPANDED
-            viewModel.mediaHost.showsOnlyActiveMedia = false
-            viewModel.mediaHost.falsingProtectionNeeded = false
-            viewModel.mediaHost.init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
             viewModel.mediaHost.hostView.layoutParams =
                 FrameLayout.LayoutParams(
                     FrameLayout.LayoutParams.MATCH_PARENT,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index 84d4246..56d6879 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -17,13 +17,16 @@
 package com.android.systemui.keyguard.ui.composable.blueprint
 
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.IntRect
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
@@ -66,6 +69,7 @@
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
         val burnIn = rememberBurnIn(clockInteractor)
+        val resources = LocalContext.current.resources
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -88,13 +92,27 @@
                             SmartSpace(
                                 burnInParams = burnIn.parameters,
                                 onTopChanged = burnIn.onSmartspaceTopChanged,
-                                modifier = Modifier.fillMaxWidth(),
+                                modifier =
+                                    Modifier.fillMaxWidth()
+                                        .padding(
+                                            top = { viewModel.getSmartSpacePaddingTop(resources) }
+                                        ),
                             )
                         }
-                        with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
-                        with(notificationSection) {
-                            Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+
+                        if (viewModel.isLargeClockVisible) {
+                            Spacer(modifier = Modifier.weight(weight = 1f))
+                            with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
                         }
+
+                        if (viewModel.areNotificationsVisible) {
+                            with(notificationSection) {
+                                Notifications(
+                                    modifier = Modifier.fillMaxWidth().weight(weight = 1f)
+                                )
+                            }
+                        }
+
                         if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
                             with(ambientIndicationSectionOptional.get()) {
                                 AmbientIndication(modifier = Modifier.fillMaxWidth())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index 4148462..d0aa444 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -17,13 +17,16 @@
 package com.android.systemui.keyguard.ui.composable.blueprint
 
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.IntRect
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
@@ -66,6 +69,7 @@
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
         val burnIn = rememberBurnIn(clockInteractor)
+        val resources = LocalContext.current.resources
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -88,13 +92,27 @@
                             SmartSpace(
                                 burnInParams = burnIn.parameters,
                                 onTopChanged = burnIn.onSmartspaceTopChanged,
-                                modifier = Modifier.fillMaxWidth(),
+                                modifier =
+                                    Modifier.fillMaxWidth()
+                                        .padding(
+                                            top = { viewModel.getSmartSpacePaddingTop(resources) }
+                                        ),
                             )
                         }
-                        with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
-                        with(notificationSection) {
-                            Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+
+                        if (viewModel.isLargeClockVisible) {
+                            Spacer(modifier = Modifier.weight(weight = 1f))
+                            with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
                         }
+
+                        if (viewModel.areNotificationsVisible) {
+                            with(notificationSection) {
+                                Notifications(
+                                    modifier = Modifier.fillMaxWidth().weight(weight = 1f)
+                                )
+                            }
+                        }
+
                         if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
                             with(ambientIndicationSectionOptional.get()) {
                                 AmbientIndication(modifier = Modifier.fillMaxWidth())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
index f021bb6..f40b871 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -30,10 +30,10 @@
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.padding
 import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.customization.R as customizationR
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.res.R
 import javax.inject.Inject
 
 class ClockSection
@@ -79,7 +79,7 @@
                     modifier =
                         Modifier.padding(
                                 horizontal =
-                                    dimensionResource(R.dimen.keyguard_affordance_horizontal_offset)
+                                    dimensionResource(customizationR.dimen.clock_padding_start)
                             )
                             .padding(top = { viewModel.getSmallClockTopMargin(view.context) })
                             .onTopPlacementChanged(onTopChanged),
@@ -117,9 +117,7 @@
             content {
                 AndroidView(
                     factory = { checkNotNull(currentClock).largeClock.view },
-                    modifier =
-                        Modifier.fillMaxWidth()
-                            .padding(top = { viewModel.getLargeClockTopMargin(view.context) })
+                    modifier = Modifier.fillMaxWidth()
                 )
             }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 0eec024..0b26ae9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -43,10 +43,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
-import com.android.compose.animation.scene.ValueKey
-import com.android.compose.animation.scene.animateElementFloatAsState
 import com.android.systemui.notifications.ui.composable.Notifications.Form
-import com.android.systemui.notifications.ui.composable.Notifications.SharedValues.SharedExpansionValue
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 
 object Notifications {
@@ -56,10 +53,6 @@
         val ShelfSpace = ElementKey("ShelfSpace")
     }
 
-    object SharedValues {
-        val SharedExpansionValue = ValueKey("SharedExpansionValue")
-    }
-
     enum class Form {
         HunFromTop,
         Stack,
@@ -181,13 +174,6 @@
                     )
                 }
     ) {
-        val animatedExpansion by
-            animateElementFloatAsState(
-                value = if (form == Form.HunFromTop) 0f else 1f,
-                key = SharedExpansionValue
-            )
-        debugLog(viewModel) { "STACK composed: expansion=$animatedExpansion" }
-
         content {
             if (viewModel.isPlaceholderTextVisible) {
                 Box(Modifier.fillMaxSize()) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 64388b7..ff05478 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -73,7 +73,7 @@
     private val positionalThreshold
         get() = with(layoutImpl.density) { 56.dp.toPx() }
 
-    internal var gestureWithPriority: Any? = null
+    internal var currentSource: Any? = null
 
     /** The [UserAction]s associated to the current swipe. */
     private var actionUpOrLeft: UserAction? = null
@@ -520,20 +520,22 @@
 private class SceneDraggableHandler(
     private val gestureHandler: SceneGestureHandler,
 ) : DraggableHandler {
+    private val source = this
+
     override fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int) {
-        gestureHandler.gestureWithPriority = this
+        gestureHandler.currentSource = source
         gestureHandler.onDragStarted(pointersDown, startedPosition, overSlop)
     }
 
     override fun onDelta(pixels: Float) {
-        if (gestureHandler.gestureWithPriority == this) {
+        if (gestureHandler.currentSource == source) {
             gestureHandler.onDrag(delta = pixels)
         }
     }
 
     override fun onDragStopped(velocity: Float) {
-        if (gestureHandler.gestureWithPriority == this) {
-            gestureHandler.gestureWithPriority = null
+        if (gestureHandler.currentSource == source) {
+            gestureHandler.currentSource = null
             gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
         }
     }
@@ -586,6 +588,8 @@
             return nextScene != null
         }
 
+        val source = this
+
         return PriorityNestedScrollConnection(
             orientation = orientation,
             canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
@@ -656,7 +660,7 @@
             canContinueScroll = { true },
             canScrollOnFling = false,
             onStart = { offsetAvailable ->
-                gestureHandler.gestureWithPriority = this
+                gestureHandler.currentSource = source
                 gestureHandler.onDragStarted(
                     pointersDown = 1,
                     startedPosition = null,
@@ -664,7 +668,7 @@
                 )
             },
             onScroll = { offsetAvailable ->
-                if (gestureHandler.gestureWithPriority != this) {
+                if (gestureHandler.currentSource != source) {
                     return@PriorityNestedScrollConnection 0f
                 }
 
@@ -675,7 +679,7 @@
                 offsetAvailable
             },
             onStop = { velocityAvailable ->
-                if (gestureHandler.gestureWithPriority != this) {
+                if (gestureHandler.currentSource != source) {
                     return@PriorityNestedScrollConnection 0f
                 }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 02d30c5e..bc3ca1b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -230,11 +230,7 @@
         sceneTransitionStateFlow =
             MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Lockscreen))
         sceneInteractor.setTransitionState(sceneTransitionStateFlow)
-        deviceEntryInteractor =
-            sceneTestUtils.deviceEntryInteractor(
-                authenticationInteractor = sceneTestUtils.authenticationInteractor(),
-                sceneInteractor = sceneInteractor,
-            )
+        deviceEntryInteractor = sceneTestUtils.deviceEntryInteractor()
 
         mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         underTest =
@@ -253,7 +249,7 @@
                 falsingManager,
                 userSwitcherController,
                 featureFlags,
-                sceneTestUtils.sceneContainerFlags,
+                sceneTestUtils.fakeSceneContainerFlags,
                 globalSettings,
                 sessionTracker,
                 Optional.of(sideFpsController),
@@ -791,6 +787,7 @@
     @Test
     fun dismissesKeyguard_whenSceneChangesToGone() =
         sceneTestUtils.testScope.runTest {
+            sceneTestUtils.fakeSceneContainerFlags.enabled = true
             // Upon init, we have never dismisses the keyguard.
             underTest.onInit()
             runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
index 74f50d8..27d1eb7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
@@ -37,11 +37,9 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ColorCorrectionRepositoryImplTest : SysuiTestCase() {
-    companion object {
-        val TEST_USER_1 = UserHandle.of(1)!!
-        val TEST_USER_2 = UserHandle.of(2)!!
-    }
 
+    private val testUser1 = UserHandle.of(1)!!
+    private val testUser2 = UserHandle.of(2)!!
     private val testDispatcher = StandardTestDispatcher()
     private val scope = TestScope(testDispatcher)
     private val settings: FakeSettings = FakeSettings()
@@ -63,7 +61,7 @@
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 1,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
 
             underTest =
@@ -72,84 +70,84 @@
                     settings,
                 )
 
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
             runCurrent()
 
-            val actualValue: Boolean = underTest.isEnabled(TEST_USER_1).first()
+            val actualValue: Boolean = underTest.isEnabled(testUser1).first()
             Truth.assertThat(actualValue).isTrue()
         }
 
     @Test
     fun isEnabled_settingUpdated_valueUpdated() =
         scope.runTest {
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
 
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.DISABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse()
 
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.ENABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isTrue()
 
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.DISABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse()
         }
 
     @Test
     fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() =
         scope.runTest {
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.DISABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
-            underTest.isEnabled(TEST_USER_2).launchIn(backgroundScope)
+            underTest.isEnabled(testUser2).launchIn(backgroundScope)
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.DISABLED,
-                TEST_USER_2.identifier
+                testUser2.identifier
             )
 
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser2).first()).isFalse()
 
             settings.putIntForUser(
                 ColorCorrectionRepositoryImpl.SETTING_NAME,
                 ColorCorrectionRepositoryImpl.ENABLED,
-                TEST_USER_1.identifier
+                testUser1.identifier
             )
             runCurrent()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
-            Truth.assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+            Truth.assertThat(underTest.isEnabled(testUser1).first()).isTrue()
+            Truth.assertThat(underTest.isEnabled(testUser2).first()).isFalse()
         }
 
     @Test
     fun setEnabled() =
         scope.runTest {
-            val success = underTest.setIsEnabled(true, TEST_USER_1)
+            val success = underTest.setIsEnabled(true, testUser1)
             runCurrent()
             Truth.assertThat(success).isTrue()
 
             val actualValue =
                 settings.getIntForUser(
                     ColorCorrectionRepositoryImpl.SETTING_NAME,
-                    TEST_USER_1.identifier
+                    testUser1.identifier
                 )
             Truth.assertThat(actualValue).isEqualTo(ColorCorrectionRepositoryImpl.ENABLED)
         }
@@ -157,14 +155,14 @@
     @Test
     fun setDisabled() =
         scope.runTest {
-            val success = underTest.setIsEnabled(false, TEST_USER_1)
+            val success = underTest.setIsEnabled(false, testUser1)
             runCurrent()
             Truth.assertThat(success).isTrue()
 
             val actualValue =
                 settings.getIntForUser(
                     ColorCorrectionRepositoryImpl.SETTING_NAME,
-                    TEST_USER_1.identifier
+                    testUser1.identifier
                 )
             Truth.assertThat(actualValue).isEqualTo(ColorCorrectionRepositoryImpl.DISABLED)
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 3f05fef..423e124 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -39,6 +39,8 @@
 @RunWith(AndroidJUnit4::class)
 class ColorInversionRepositoryImplTest : SysuiTestCase() {
 
+    private val testUser1 = UserHandle.of(1)!!
+    private val testUser2 = UserHandle.of(2)!!
     private val testDispatcher = StandardTestDispatcher()
     private val scope = TestScope(testDispatcher)
     private val settings: FakeSettings = FakeSettings()
@@ -57,7 +59,7 @@
     @Test
     fun isEnabled_initiallyGetsSettingsValue() =
         scope.runTest {
-            settings.putIntForUser(SETTING_NAME, 1, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, 1, testUser1.identifier)
 
             underTest =
                 ColorInversionRepositoryImpl(
@@ -65,68 +67,68 @@
                     settings,
                 )
 
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
             runCurrent()
 
-            val actualValue: Boolean = underTest.isEnabled(TEST_USER_1).first()
+            val actualValue: Boolean = underTest.isEnabled(testUser1).first()
             assertThat(actualValue).isTrue()
         }
 
     @Test
     fun isEnabled_settingUpdated_valueUpdated() =
         scope.runTest {
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
 
-            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser1).first()).isFalse()
 
-            settings.putIntForUser(SETTING_NAME, ENABLED, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
+            assertThat(underTest.isEnabled(testUser1).first()).isTrue()
 
-            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser1).first()).isFalse()
         }
 
     @Test
     fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() =
         scope.runTest {
-            underTest.isEnabled(TEST_USER_1).launchIn(backgroundScope)
-            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_1.identifier)
-            underTest.isEnabled(TEST_USER_2).launchIn(backgroundScope)
-            settings.putIntForUser(SETTING_NAME, DISABLED, TEST_USER_2.identifier)
+            underTest.isEnabled(testUser1).launchIn(backgroundScope)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
+            underTest.isEnabled(testUser2).launchIn(backgroundScope)
+            settings.putIntForUser(SETTING_NAME, DISABLED, testUser2.identifier)
 
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isFalse()
-            assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser1).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser2).first()).isFalse()
 
-            settings.putIntForUser(SETTING_NAME, ENABLED, TEST_USER_1.identifier)
+            settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
             runCurrent()
-            assertThat(underTest.isEnabled(TEST_USER_1).first()).isTrue()
-            assertThat(underTest.isEnabled(TEST_USER_2).first()).isFalse()
+            assertThat(underTest.isEnabled(testUser1).first()).isTrue()
+            assertThat(underTest.isEnabled(testUser2).first()).isFalse()
         }
 
     @Test
     fun setEnabled() =
         scope.runTest {
-            val success = underTest.setIsEnabled(true, TEST_USER_1)
+            val success = underTest.setIsEnabled(true, testUser1)
             runCurrent()
             assertThat(success).isTrue()
 
-            val actualValue = settings.getIntForUser(SETTING_NAME, TEST_USER_1.identifier)
+            val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier)
             assertThat(actualValue).isEqualTo(ENABLED)
         }
 
     @Test
     fun setDisabled() =
         scope.runTest {
-            val success = underTest.setIsEnabled(false, TEST_USER_1)
+            val success = underTest.setIsEnabled(false, testUser1)
             runCurrent()
             assertThat(success).isTrue()
 
-            val actualValue = settings.getIntForUser(SETTING_NAME, TEST_USER_1.identifier)
+            val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier)
             assertThat(actualValue).isEqualTo(DISABLED)
         }
 
@@ -134,7 +136,5 @@
         private const val SETTING_NAME = ACCESSIBILITY_DISPLAY_INVERSION_ENABLED
         private const val DISABLED = 0
         private const val ENABLED = 1
-        private val TEST_USER_1 = UserHandle.of(1)!!
-        private val TEST_USER_2 = UserHandle.of(2)!!
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index b9759cc..b4d4e1f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -83,7 +83,7 @@
             AuthenticationRepositoryImpl(
                 applicationScope = testScope.backgroundScope,
                 backgroundDispatcher = testUtils.testDispatcher,
-                flags = testUtils.sceneContainerFlags,
+                flags = testUtils.fakeSceneContainerFlags,
                 clock = clock,
                 getSecurityMode = getSecurityMode,
                 userRepository = userRepository,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 15633d1..4a39799 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -18,6 +18,7 @@
 
 import android.hardware.biometrics.BiometricManager.Authenticators
 import android.hardware.biometrics.ComponentInfoInternal
+import android.hardware.biometrics.PromptContentView
 import android.hardware.biometrics.PromptInfo
 import android.hardware.biometrics.SensorProperties
 import android.hardware.biometrics.SensorPropertiesInternal
@@ -119,6 +120,7 @@
     title: String = "title",
     subtitle: String = "sub",
     description: String = "desc",
+    contentView: PromptContentView? = null,
     credentialTitle: String? = "cred title",
     credentialSubtitle: String? = "cred sub",
     credentialDescription: String? = "cred desc",
@@ -128,6 +130,7 @@
     info.title = title
     info.subtitle = subtitle
     info.description = description
+    info.contentView = contentView
     credentialTitle?.let { info.deviceCredentialTitle = it }
     credentialSubtitle?.let { info.deviceCredentialSubtitle = it }
     credentialDescription?.let { info.deviceCredentialDescription = it }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index b0beab9..f7743e2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
 import com.android.systemui.biometrics.ui.viewmodel.DefaultUdfpsTouchOverlayViewModel
 import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel
@@ -118,6 +119,7 @@
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock private lateinit var shadeInteractor: ShadeInteractor
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
+    @Mock private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
     private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams()
@@ -174,7 +176,8 @@
                 mSelectedUserInteractor,
                 { deviceEntryUdfpsTouchOverlayViewModel },
                 { defaultUdfpsTouchOverlayViewModel },
-                shadeInteractor
+                shadeInteractor,
+                udfpsOverlayInteractor,
             )
         block()
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index a59a4b8..90c3c14 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -76,6 +76,7 @@
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
 import com.android.systemui.biometrics.udfps.InteractionEvent;
 import com.android.systemui.biometrics.udfps.NormalizedTouchData;
@@ -214,6 +215,8 @@
     @Mock
     private AlternateBouncerInteractor mAlternateBouncerInteractor;
     @Mock
+    private UdfpsOverlayInteractor mUdfpsOverlayInteractor;
+    @Mock
     private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     @Mock
     private SelectedUserInteractor mSelectedUserInteractor;
@@ -342,8 +345,8 @@
                 mFpsUnlockTracker,
                 mKeyguardTransitionInteractor,
                 mDeviceEntryUdfpsTouchOverlayViewModel,
-                mDefaultUdfpsTouchOverlayViewModel
-
+                mDefaultUdfpsTouchOverlayViewModel,
+                mUdfpsOverlayInteractor
         );
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
index 13b53a8..7d9c2f9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
@@ -27,6 +27,7 @@
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.dump.DumpManager;
@@ -76,6 +77,7 @@
     protected @Mock UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     protected @Mock SelectedUserInteractor mSelectedUserInteractor;
     protected @Mock KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    protected @Mock UdfpsOverlayInteractor mUdfpsOverlayInteractor;
 
     protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
@@ -152,7 +154,8 @@
                 mUdfpsKeyguardAccessibilityDelegate,
                 mSelectedUserInteractor,
                 mKeyguardTransitionInteractor,
-                mShadeInteractor);
+                mShadeInteractor,
+                mUdfpsOverlayInteractor);
         return controller;
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 335ac9d..0e257bc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATE_APPEAR_ON_SCREEN_OFF
 import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepositoryImpl
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
@@ -32,6 +33,7 @@
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
 import com.android.systemui.bouncer.ui.BouncerView
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
@@ -45,9 +47,11 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.time.SystemClock
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -130,6 +134,13 @@
                     repository = transitionRepository,
                 )
                 .keyguardTransitionInteractor
+        mUdfpsOverlayInteractor =
+            UdfpsOverlayInteractor(
+                context,
+                mock(AuthController::class.java),
+                mock(SelectedUserInteractor::class.java),
+                testScope.backgroundScope,
+            )
         return createUdfpsKeyguardViewController(/* useModernBouncer */ true)
     }
 
@@ -239,6 +250,31 @@
         }
 
     @Test
+    fun shouldHandleTouchesChange() =
+        testScope.runTest {
+            val shouldHandleTouches by collectLastValue(mUdfpsOverlayInteractor.shouldHandleTouches)
+
+            // GIVEN view is attached + on the keyguard
+            mController.onViewAttached()
+            captureStatusBarStateListeners()
+            sendStatusBarStateChanged(StatusBarState.KEYGUARD)
+            whenever(mView.setPauseAuth(true)).thenReturn(true)
+            whenever(mView.unpausedAlpha).thenReturn(0)
+
+            // WHEN panelViewExpansion changes to expanded
+            val job = mController.listenForBouncerExpansion(this)
+            keyguardBouncerRepository.setPrimaryShow(true)
+            keyguardBouncerRepository.setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
+            runCurrent()
+
+            // THEN UDFPS auth is paused and should not handle touches
+            assertThat(mController.shouldPauseAuth()).isTrue()
+            assertThat(shouldHandleTouches!!).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun fadeFromDialogSuggestedAlpha() =
         testScope.runTest {
             // GIVEN view is attached and status bar expansion is 1f
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt
new file mode 100644
index 0000000..ccf119a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.biometrics.domain.interactor
+
+import android.hardware.biometrics.SensorLocationInternal
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FingerprintPropertyInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fingerprintPropertyInteractor
+    private val repository = kosmos.fingerprintPropertyRepository
+    private val configurationRepository = kosmos.fakeConfigurationRepository
+    private val displayRepository = kosmos.displayRepository
+
+    @Test
+    fun sensorLocation_resolution1f() =
+        testScope.runTest {
+            val currSensorLocation by collectLastValue(underTest.sensorLocation)
+
+            displayRepository.emitDisplayChangeEvent(0)
+            runCurrent()
+            repository.setProperties(
+                sensorId = 0,
+                strength = SensorStrength.STRONG,
+                sensorType = FingerprintSensorType.UDFPS_OPTICAL,
+                sensorLocations =
+                    mapOf(
+                        Pair("", SensorLocationInternal("", 4, 4, 2)),
+                        Pair("otherDisplay", SensorLocationInternal("", 1, 1, 1))
+                    )
+            )
+            runCurrent()
+            configurationRepository.setScaleForResolution(1f)
+            runCurrent()
+
+            assertThat(currSensorLocation?.centerX).isEqualTo(4)
+            assertThat(currSensorLocation?.centerY).isEqualTo(4)
+            assertThat(currSensorLocation?.radius).isEqualTo(2)
+        }
+
+    @Test
+    fun sensorLocation_resolution2f() =
+        testScope.runTest {
+            val currSensorLocation by collectLastValue(underTest.sensorLocation)
+
+            displayRepository.emitDisplayChangeEvent(0)
+            runCurrent()
+            repository.setProperties(
+                sensorId = 0,
+                strength = SensorStrength.STRONG,
+                sensorType = FingerprintSensorType.UDFPS_OPTICAL,
+                sensorLocations =
+                    mapOf(
+                        Pair("", SensorLocationInternal("", 4, 4, 2)),
+                        Pair("otherDisplay", SensorLocationInternal("", 1, 1, 1))
+                    )
+            )
+            runCurrent()
+            configurationRepository.setScaleForResolution(2f)
+            runCurrent()
+
+            assertThat(currSensorLocation?.centerX).isEqualTo(4 * 2)
+            assertThat(currSensorLocation?.centerY).isEqualTo(4 * 2)
+            assertThat(currSensorLocation?.radius).isEqualTo(2 * 2)
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
index 67ce86b..63581b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
@@ -16,27 +16,25 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
-import android.app.ActivityTaskManager
 import android.telecom.TelecomManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.app.activityTaskManager
 import com.android.internal.R
+import com.android.internal.logging.fakeMetricsLogger
 import com.android.internal.logging.nano.MetricsProto
-import com.android.internal.logging.testing.FakeMetricsLogger
-import com.android.internal.util.EmergencyAffordanceManager
+import com.android.internal.util.emergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags.REFACTOR_GETCURRENTUSER
-import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.whenever
+import com.android.telecom.telecomManager
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -53,27 +51,26 @@
 @RunWith(AndroidJUnit4::class)
 class BouncerActionButtonInteractorTest : SysuiTestCase() {
 
-    @Mock private lateinit var activityTaskManager: ActivityTaskManager
-    @Mock private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
     @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
-    @Mock private lateinit var tableLogger: TableLogBuffer
     @Mock private lateinit var telecomManager: TelecomManager
 
-    private lateinit var utils: SceneTestUtils
-    private lateinit var testScope: TestScope
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
+    private val metricsLogger = utils.kosmos.fakeMetricsLogger
+    private val activityTaskManager = utils.kosmos.activityTaskManager
+    private val emergencyAffordanceManager = utils.kosmos.emergencyAffordanceManager
+
     private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
 
-    private val metricsLogger = FakeMetricsLogger()
     private var currentUserId: Int = 0
     private var needsEmergencyAffordance = true
 
-    private lateinit var underTest: BouncerActionButtonInteractor
-
     @Before
     fun setUp() {
-        utils = SceneTestUtils(this)
-        testScope = utils.testScope
         MockitoAnnotations.initMocks(this)
+        utils.fakeSceneContainerFlags.enabled = true
+
+        mobileConnectionsRepository = utils.mobileConnectionsRepository
 
         overrideResource(R.string.lockscreen_emergency_call, MESSAGE_EMERGENCY_CALL)
         overrideResource(R.string.lockscreen_return_to_call, MESSAGE_RETURN_TO_CALL)
@@ -86,34 +83,18 @@
             .thenReturn(needsEmergencyAffordance)
         whenever(telecomManager.isInCall).thenReturn(false)
 
-        utils.featureFlags.set(REFACTOR_GETCURRENTUSER, true)
-
-        mobileConnectionsRepository =
-            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
+        utils.fakeFeatureFlags.set(REFACTOR_GETCURRENTUSER, true)
 
         utils.telephonyRepository.setHasTelephonyRadio(true)
 
-        underTest =
-            utils.bouncerActionButtonInteractor(
-                mobileConnectionsRepository = mobileConnectionsRepository,
-                activityTaskManager = activityTaskManager,
-                telecomManager = telecomManager,
-                emergencyAffordanceManager = emergencyAffordanceManager,
-                metricsLogger = metricsLogger,
-            )
+        utils.kosmos.telecomManager = telecomManager
     }
 
     @Test
     fun noTelephonyRadio_noButton() =
         testScope.runTest {
             utils.telephonyRepository.setHasTelephonyRadio(false)
-            underTest =
-                utils.bouncerActionButtonInteractor(
-                    mobileConnectionsRepository = mobileConnectionsRepository,
-                    activityTaskManager = activityTaskManager,
-                    telecomManager = telecomManager,
-                )
-
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             assertThat(actionButton).isNull()
         }
@@ -121,12 +102,8 @@
     @Test
     fun noTelecomManager_noButton() =
         testScope.runTest {
-            underTest =
-                utils.bouncerActionButtonInteractor(
-                    mobileConnectionsRepository = mobileConnectionsRepository,
-                    activityTaskManager = activityTaskManager,
-                    telecomManager = null,
-                )
+            utils.kosmos.telecomManager = null
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             assertThat(actionButton).isNull()
         }
@@ -134,6 +111,7 @@
     @Test
     fun duringCall_returnToCallButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             utils.telephonyRepository.setIsInCall(true)
 
@@ -143,6 +121,7 @@
             assertThat(actionButton?.onLongClick).isNull()
 
             actionButton?.onClick?.invoke()
+            runCurrent()
 
             assertThat(metricsLogger.logs.size).isEqualTo(1)
             assertThat(metricsLogger.logs.element().category)
@@ -154,6 +133,7 @@
     @Test
     fun noCall_secureAuthMethod_emergencyCallButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             mobileConnectionsRepository.isAnySimSecure.value = false
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
@@ -165,6 +145,7 @@
             assertThat(actionButton?.onLongClick).isNotNull()
 
             actionButton?.onClick?.invoke()
+            runCurrent()
 
             assertThat(metricsLogger.logs.size).isEqualTo(1)
             assertThat(metricsLogger.logs.element().category)
@@ -182,10 +163,12 @@
     @Test
     fun noCall_insecureAuthMethodButSecureSim_emergencyCallButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             mobileConnectionsRepository.isAnySimSecure.value = true
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
             utils.telephonyRepository.setIsInCall(false)
+            runCurrent()
 
             assertThat(actionButton).isNotNull()
             assertThat(actionButton?.label).isEqualTo(MESSAGE_EMERGENCY_CALL)
@@ -196,6 +179,7 @@
     @Test
     fun noCall_insecure_noButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             mobileConnectionsRepository.isAnySimSecure.value = false
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
@@ -207,6 +191,7 @@
     @Test
     fun noCall_simSecureButEmergencyNotSupported_noButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             mobileConnectionsRepository.isAnySimSecure.value = true
             overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 93ba6a4..4b6199b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -25,7 +25,8 @@
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
 import com.google.common.truth.Truth.assertThat
@@ -37,8 +38,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -46,9 +45,7 @@
 @RunWith(AndroidJUnit4::class)
 class BouncerInteractorTest : SysuiTestCase() {
 
-    @Mock private lateinit var mDeviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor
-
-    private val utils = SceneTestUtils(this)
+    private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true }
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
 
@@ -57,6 +54,7 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+
         overrideResource(R.string.keyguard_enter_your_pin, MESSAGE_ENTER_YOUR_PIN)
         overrideResource(R.string.keyguard_enter_your_password, MESSAGE_ENTER_YOUR_PASSWORD)
         overrideResource(R.string.keyguard_enter_your_pattern, MESSAGE_ENTER_YOUR_PATTERN)
@@ -64,11 +62,7 @@
         overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD)
         overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN)
 
-        underTest =
-            utils.bouncerInteractor(
-                authenticationInteractor = authenticationInteractor,
-                deviceEntryFaceAuthInteractor = mDeviceEntryFaceAuthInteractor,
-            )
+        underTest = utils.bouncerInteractor()
     }
 
     @Test
@@ -305,8 +299,16 @@
     @Test
     fun intentionalUserInputEvent_notifiesFaceAuthInteractor() =
         testScope.runTest {
+            val isFaceAuthRunning by
+                collectLastValue(utils.kosmos.fakeDeviceEntryFaceAuthRepository.isAuthRunning)
+            utils.kosmos.deviceEntryFaceAuthInteractor.onDeviceLifted()
+            runCurrent()
+            assertThat(isFaceAuthRunning).isTrue()
+
             underTest.onIntentionalUserInput()
-            verify(mDeviceEntryFaceAuthInteractor).onPrimaryBouncerUserInput()
+            runCurrent()
+
+            assertThat(isFaceAuthRunning).isFalse()
         }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
similarity index 99%
rename from packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
index ee46f76..63f6c20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.testing.TestableResources
 import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -60,7 +60,7 @@
 
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 class PrimaryBouncerInteractorTest : SysuiTestCase() {
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private lateinit var repository: KeyguardBouncerRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index 2f0843b..3043a71 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -35,10 +35,7 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = utils.authenticationInteractor(),
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
     private val underTest =
         PinBouncerViewModel(
             applicationContext = context,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 47bbe6f4..4b1f9fe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -41,6 +41,7 @@
 import kotlinx.coroutines.test.currentTime
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -52,15 +53,14 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
-    private val underTest =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private lateinit var underTest: BouncerViewModel
+
+    @Before
+    fun setUp() {
+        utils.fakeSceneContainerFlags.enabled = true
+        underTest = utils.bouncerViewModel()
+    }
 
     @Test
     fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() =
@@ -228,13 +228,13 @@
     fun isSideBySideSupported() =
         testScope.runTest {
             val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported)
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             utils.authenticationRepository.setAuthenticationMethod(Pin)
             assertThat(isSideBySideSupported).isTrue()
             utils.authenticationRepository.setAuthenticationMethod(Password)
             assertThat(isSideBySideSupported).isTrue()
 
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
             utils.authenticationRepository.setAuthenticationMethod(Pin)
             assertThat(isSideBySideSupported).isTrue()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 64e6e57..5c5632f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -47,16 +47,8 @@
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
     private val sceneInteractor = utils.sceneInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
-    private val bouncerViewModel =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private val bouncerViewModel = utils.bouncerViewModel()
     private val isInputEnabled = MutableStateFlow(true)
 
     private val underTest =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 83d1938..9ee344a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -48,16 +48,8 @@
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
     private val sceneInteractor = utils.sceneInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
-    private val bouncerViewModel =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private val bouncerViewModel = utils.bouncerViewModel()
     private val underTest =
         PatternBouncerViewModel(
             applicationContext = context,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index db98d76..75e372f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -47,16 +47,8 @@
     private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
-    private val bouncerViewModel =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private val bouncerViewModel = utils.bouncerViewModel()
     private val underTest =
         PinBouncerViewModel(
             applicationContext = context,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 744b65f..cd83c07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -39,6 +40,9 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -69,9 +73,9 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        testScope = TestScope()
+        testScope = TestScope(StandardTestDispatcher())
 
-        val withDeps = CommunalInteractorFactory.create()
+        val withDeps = CommunalInteractorFactory.create(testScope)
 
         tutorialRepository = withDeps.tutorialRepository
         communalRepository = withDeps.communalRepository
@@ -379,6 +383,131 @@
         }
 
     @Test
+    fun transitionProgress_onTargetScene_fullProgress() =
+        testScope.runTest {
+            val targetScene = CommunalSceneKey.Blank
+            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
+            val transitionProgress by collectLastValue(transitionProgressFlow)
+
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(targetScene)
+                )
+            underTest.setTransitionState(transitionState)
+
+            // We're on the target scene.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+        }
+
+    @Test
+    fun transitionProgress_notOnTargetScene_noProgress() =
+        testScope.runTest {
+            val targetScene = CommunalSceneKey.Blank
+            val currentScene = CommunalSceneKey.Communal
+            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
+            val transitionProgress by collectLastValue(transitionProgressFlow)
+
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Transition progress is still idle, but we're not on the target scene.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+        }
+
+    @Test
+    fun transitionProgress_transitioningToTrackedScene() =
+        testScope.runTest {
+            val currentScene = CommunalSceneKey.Communal
+            val targetScene = CommunalSceneKey.Blank
+            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
+            val transitionProgress by collectLastValue(transitionProgressFlow)
+
+            var transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Progress starts at 0.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+
+            val progress = MutableStateFlow(0f)
+            transitionState =
+                MutableStateFlow(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Partially transition.
+            progress.value = .4f
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Transition(.4f))
+
+            // Transition is at full progress.
+            progress.value = 1f
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Transition(1f))
+
+            // Transition finishes.
+            transitionState = MutableStateFlow(ObservableCommunalTransitionState.Idle(targetScene))
+            underTest.setTransitionState(transitionState)
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+        }
+
+    @Test
+    fun transitionProgress_transitioningAwayFromTrackedScene() =
+        testScope.runTest {
+            val currentScene = CommunalSceneKey.Blank
+            val targetScene = CommunalSceneKey.Communal
+            val transitionProgressFlow = underTest.transitionProgressToScene(currentScene)
+            val transitionProgress by collectLastValue(transitionProgressFlow)
+
+            var transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Progress starts at 0.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+
+            val progress = MutableStateFlow(0f)
+            transitionState =
+                MutableStateFlow(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            underTest.setTransitionState(transitionState)
+
+            // Partially transition.
+            progress.value = .4f
+
+            // This is a transition we don't care about the progress of.
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.OtherTransition)
+
+            // Transition is at full progress.
+            progress.value = 1f
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.OtherTransition)
+
+            // Transition finishes.
+            transitionState = MutableStateFlow(ObservableCommunalTransitionState.Idle(targetScene))
+            underTest.setTransitionState(transitionState)
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+        }
+
+    @Test
     fun isCommunalShowing() =
         testScope.runTest {
             var isCommunalShowing = collectLastValue(underTest.isCommunalShowing)
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 a776062..804c052 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
@@ -34,6 +34,7 @@
 import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
 import com.android.systemui.util.mockito.mock
@@ -48,6 +49,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -92,6 +94,13 @@
     }
 
     @Test
+    fun init_initsMediaHost() =
+        testScope.runTest {
+            // MediaHost is initialized as soon as the class is created.
+            verify(mediaHost).init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
+        }
+
+    @Test
     fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() =
         testScope.runTest {
             // Keyguard showing, and tutorial not started.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index ea19cb7..929e879 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -23,9 +23,8 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -33,6 +32,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -43,19 +43,17 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
-    private val repository: FakeDeviceEntryRepository = utils.deviceEntryRepository
-    private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val trustRepository = FakeTrustRepository()
+    private val faceAuthRepository = utils.kosmos.fakeDeviceEntryFaceAuthRepository
+    private val trustRepository = utils.kosmos.fakeTrustRepository
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val underTest =
-        utils.deviceEntryInteractor(
-            repository = repository,
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-            faceAuthRepository = faceAuthRepository,
-            trustRepository = trustRepository,
-        )
+    private lateinit var underTest: DeviceEntryInteractor
+
+    @Before
+    fun setUp() {
+        utils.fakeSceneContainerFlags.enabled = true
+        underTest = utils.deviceEntryInteractor()
+    }
 
     @Test
     fun canSwipeToEnter_startsNull() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index 11939c1..8933d2c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -63,7 +63,7 @@
             repository = repository,
             commandQueue = commandQueue,
             powerInteractor = PowerInteractorFactory.create().powerInteractor,
-            sceneContainerFlags = testUtils.sceneContainerFlags,
+            sceneContainerFlags = testUtils.fakeSceneContainerFlags,
             bouncerRepository = bouncerRepository,
             configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()),
             shadeRepository = shadeRepository,
@@ -183,6 +183,7 @@
     @Test
     fun animationDozingTransitions() =
         testScope.runTest {
+            testUtils.fakeSceneContainerFlags.enabled = true
             val isAnimate by collectLastValue(underTest.animateDozingTransitions)
 
             underTest.setAnimateDozingTransitions(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 4f7d944..6828041 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -21,23 +21,24 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
 import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -46,18 +47,11 @@
 @kotlinx.coroutines.ExperimentalCoroutinesApi
 class KeyguardTransitionInteractorTest : SysuiTestCase() {
 
-    private lateinit var underTest: KeyguardTransitionInteractor
-    private lateinit var repository: FakeKeyguardTransitionRepository
-    private val testScope = TestScope()
+    val kosmos = testKosmos()
 
-    @Before
-    fun setUp() {
-        repository = FakeKeyguardTransitionRepository()
-        underTest = KeyguardTransitionInteractorFactory.create(
-                scope = testScope.backgroundScope,
-                repository = repository,
-        ).keyguardTransitionInteractor
-    }
+    val underTest = kosmos.keyguardTransitionInteractor
+    val repository = kosmos.fakeKeyguardTransitionRepository
+    val testScope = kosmos.testScope
 
     @Test
     fun transitionCollectorsReceivesOnlyAppropriateEvents() = runTest {
@@ -114,48 +108,50 @@
     }
 
     @Test
-    fun finishedKeyguardStateTests() = testScope.runTest {
-        val finishedSteps by collectValues(underTest.finishedKeyguardState)
-        runCurrent()
-        val steps = mutableListOf<TransitionStep>()
-
-        steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED))
-        steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING))
-        steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED))
-        steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED))
-        steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING))
-        steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED))
-        steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
-
-        steps.forEach {
-            repository.sendTransitionStep(it)
+    fun finishedKeyguardStateTests() =
+        testScope.runTest {
+            val finishedSteps by collectValues(underTest.finishedKeyguardState)
             runCurrent()
-        }
+            val steps = mutableListOf<TransitionStep>()
 
-        assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD))
-    }
+            steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED))
+            steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED))
+            steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED))
+            steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING))
+            steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED))
+            steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+            steps.forEach {
+                repository.sendTransitionStep(it)
+                runCurrent()
+            }
+
+            assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD))
+        }
 
     @Test
-    fun startedKeyguardStateTests() = testScope.runTest {
-        val startedStates by collectValues(underTest.startedKeyguardState)
-        runCurrent()
-        val steps = mutableListOf<TransitionStep>()
-
-        steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED))
-        steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING))
-        steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED))
-        steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED))
-        steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING))
-        steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED))
-        steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
-
-        steps.forEach {
-            repository.sendTransitionStep(it)
+    fun startedKeyguardStateTests() =
+        testScope.runTest {
+            val startedStates by collectValues(underTest.startedKeyguardState)
             runCurrent()
-        }
+            val steps = mutableListOf<TransitionStep>()
 
-        assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE))
-    }
+            steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED))
+            steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED))
+            steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED))
+            steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING))
+            steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED))
+            steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+            steps.forEach {
+                repository.sendTransitionStep(it)
+                runCurrent()
+            }
+
+            assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE))
+        }
 
     @Test
     fun finishedKeyguardTransitionStepTests() = runTest {
@@ -178,7 +174,7 @@
 
         // Ignore the default state.
         assertThat(finishedSteps.subList(1, finishedSteps.size))
-                .isEqualTo(listOf(steps[2], steps[5]))
+            .isEqualTo(listOf(steps[2], steps[5]))
     }
 
     @Test
@@ -233,500 +229,1067 @@
     }
 
     @Test
-    fun isInTransitionToState() = testScope.runTest {
-        val results by collectValues(underTest.isInTransitionToState(GONE))
+    fun isInTransitionToAnyState() =
+        testScope.runTest {
+            val inTransition by collectValues(underTest.isInTransitionToAnyState)
 
-        sendSteps(
+            assertEquals(
+                listOf(
+                    true, // The repo is seeded with a transition from OFF to LOCKSCREEN.
+                    false,
+                ),
+                inTransition
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+            )
+
+            assertEquals(
+                listOf(
+                    true,
+                    false,
+                    true,
+                ),
+                inTransition
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+            )
+
+            assertEquals(
+                listOf(
+                    true,
+                    false,
+                    true,
+                ),
+                inTransition
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+            )
+
+            assertEquals(
+                listOf(
+                    true,
+                    false,
+                    true,
+                    false,
+                ),
+                inTransition
+            )
+        }
+
+    @Test
+    fun isInTransitionToAnyState_finishedStateIsStartedStateAfterCancels() =
+        testScope.runTest {
+            val inTransition by collectValues(underTest.isInTransitionToAnyState)
+
+            assertEquals(
+                listOf(
+                    true,
+                    false,
+                ),
+                inTransition
+            )
+
+            // Start FINISHED in GONE.
+            sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+                TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+                TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+            )
+
+            assertEquals(
+                listOf(
+                    true,
+                    false,
+                    true,
+                    false,
+                ),
+                inTransition
+            )
+
+            sendSteps(
+                TransitionStep(GONE, DOZING, 0f, STARTED),
+            )
+
+            assertEquals(
+                listOf(
+                    true,
+                    false,
+                    true,
+                    false,
+                    true,
+                ),
+                inTransition
+            )
+
+            sendSteps(
+                TransitionStep(GONE, DOZING, 0.5f, RUNNING),
+                TransitionStep(GONE, DOZING, 0.6f, CANCELED),
+                TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
+                TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING),
+                TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED),
+                TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+            )
+
+            assertEquals(
+                listOf(
+                    true,
+                    false,
+                    true,
+                    false,
+                    // We should have been in transition throughout the entire transition, including
+                    // both cancellations, and we should still be in transition despite now
+                    // transitioning to GONE, the state we're also FINISHED in.
+                    true,
+                ),
+                inTransition
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+                TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+            )
+
+            assertEquals(
+                listOf(
+                    true,
+                    false,
+                    true,
+                    false,
+                    true,
+                    false,
+                ),
+                inTransition
+            )
+        }
+
+    @Test
+    fun isInTransitionToState() =
+        testScope.runTest {
+            val results by collectValues(underTest.isInTransitionToState(GONE))
+
+            sendSteps(
                 TransitionStep(AOD, DOZING, 0f, STARTED),
                 TransitionStep(AOD, DOZING, 0.5f, RUNNING),
                 TransitionStep(AOD, DOZING, 1f, FINISHED),
-        )
+            )
 
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-        ))
-
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(GONE, DOZING, 0f, STARTED),
                 TransitionStep(GONE, DOZING, 0f, RUNNING),
                 TransitionStep(GONE, DOZING, 1f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-                true,
-        ))
-    }
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                        true,
+                    )
+                )
+        }
 
     @Test
-    fun isInTransitionFromState() = testScope.runTest {
-        val results by collectValues(underTest.isInTransitionFromState(DOZING))
+    fun isInTransitionFromState() =
+        testScope.runTest {
+            val results by collectValues(underTest.isInTransitionFromState(DOZING))
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(AOD, DOZING, 0f, STARTED),
                 TransitionStep(AOD, DOZING, 0.5f, RUNNING),
                 TransitionStep(AOD, DOZING, 1f, FINISHED),
-        )
+            )
 
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-        ))
-
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(GONE, DOZING, 0f, STARTED),
                 TransitionStep(GONE, DOZING, 0f, RUNNING),
                 TransitionStep(GONE, DOZING, 1f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-                true,
-        ))
-    }
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                        true,
+                    )
+                )
+        }
 
     @Test
-    fun isInTransitionFromStateWhere() = testScope.runTest {
-        val results by collectValues(underTest.isInTransitionFromStateWhere {
-            it == DOZING
-        })
+    fun isInTransitionFromStateWhere() =
+        testScope.runTest {
+            val results by collectValues(underTest.isInTransitionFromStateWhere { it == DOZING })
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(AOD, DOZING, 0f, STARTED),
                 TransitionStep(AOD, DOZING, 0.5f, RUNNING),
                 TransitionStep(AOD, DOZING, 1f, FINISHED),
-        )
+            )
 
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-        ))
-
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(GONE, DOZING, 0f, STARTED),
                 TransitionStep(GONE, DOZING, 0f, RUNNING),
                 TransitionStep(GONE, DOZING, 1f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-                true,
-        ))
-    }
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                        true,
+                    )
+                )
+        }
 
     @Test
-    fun isInTransitionWhere() = testScope.runTest {
-        val results by collectValues(underTest.isInTransitionWhere(
-            fromStatePredicate = { it == DOZING },
-            toStatePredicate = { it == GONE },
-        ))
+    fun isInTransitionWhere() =
+        testScope.runTest {
+            val results by
+                collectValues(
+                    underTest.isInTransitionWhere(
+                        fromStatePredicate = { it == DOZING },
+                        toStatePredicate = { it == GONE },
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(AOD, DOZING, 0f, STARTED),
                 TransitionStep(AOD, DOZING, 0.5f, RUNNING),
                 TransitionStep(AOD, DOZING, 1f, FINISHED),
-        )
+            )
 
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-        ))
-
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(GONE, DOZING, 0f, STARTED),
                 TransitionStep(GONE, DOZING, 0f, RUNNING),
                 TransitionStep(GONE, DOZING, 1f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-                true,
-        ))
-    }
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                        true,
+                    )
+                )
+        }
 
     @Test
-    fun isFinishedInStateWhere() = testScope.runTest {
-        val results by collectValues(underTest.isFinishedInStateWhere { it == GONE } )
+    fun isInTransitionWhere_withCanceledStep() =
+        testScope.runTest {
+            val results by
+                collectValues(
+                    underTest.isInTransitionWhere(
+                        fromStatePredicate = { it == DOZING },
+                        toStatePredicate = { it == GONE },
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(AOD, DOZING, 0f, STARTED),
                 TransitionStep(AOD, DOZING, 0.5f, RUNNING),
                 TransitionStep(AOD, DOZING, 1f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false, // Finished in DOZING, not GONE.
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
 
-        sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
+            sendSteps(
+                TransitionStep(DOZING, GONE, 0f, STARTED),
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
+            sendSteps(
+                TransitionStep(DOZING, GONE, 0f, RUNNING),
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+            sendSteps(
+                TransitionStep(DOZING, GONE, 0f, CANCELED),
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(GONE, DOZING, 0f, STARTED),
                 TransitionStep(GONE, DOZING, 0f, RUNNING),
-        )
+                TransitionStep(GONE, DOZING, 1f, FINISHED),
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
-
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
-
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
-
-        sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
-
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-                true,
-        ))
-    }
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                        true,
+                    )
+                )
+        }
 
     @Test
-    fun isFinishedInState() = testScope.runTest {
-        val results by collectValues(underTest.isFinishedInState(GONE))
+    fun isFinishedInStateWhere() =
+        testScope.runTest {
+            val results by collectValues(underTest.isFinishedInStateWhere { it == GONE })
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(AOD, DOZING, 0f, STARTED),
                 TransitionStep(AOD, DOZING, 0.5f, RUNNING),
                 TransitionStep(AOD, DOZING, 1f, FINISHED),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false, // Finished in DOZING, not GONE.
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false, // Finished in DOZING, not GONE.
+                    )
+                )
 
-        sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
+            sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
 
-        sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
+            sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
 
-        sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+            sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(GONE, DOZING, 0f, STARTED),
                 TransitionStep(GONE, DOZING, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
 
-        sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
+            sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(DOZING, GONE, 0f, STARTED),
                 TransitionStep(DOZING, GONE, 0f, RUNNING),
-        )
+            )
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-        ))
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
 
-        sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+            sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
 
-        assertThat(results).isEqualTo(listOf(
-                false,
-                true,
-                false,
-                true,
-        ))
-    }
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                        true,
+                    )
+                )
+        }
 
     @Test
-    fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = testScope.runTest {
-        val finishedStates by collectValues(underTest.finishedKeyguardState)
+    fun isFinishedInState() =
+        testScope.runTest {
+            val results by collectValues(underTest.isFinishedInState(GONE))
 
-        // We default FINISHED in LOCKSCREEN.
-        assertEquals(listOf(
-                LOCKSCREEN
-        ), finishedStates)
+            sendSteps(
+                TransitionStep(AOD, DOZING, 0f, STARTED),
+                TransitionStep(AOD, DOZING, 0.5f, RUNNING),
+                TransitionStep(AOD, DOZING, 1f, FINISHED),
+            )
 
-        sendSteps(
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false, // Finished in DOZING, not GONE.
+                    )
+                )
+
+            sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
+
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
+
+            sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
+
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                    )
+                )
+
+            sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
+
+            sendSteps(
+                TransitionStep(GONE, DOZING, 0f, STARTED),
+                TransitionStep(GONE, DOZING, 0f, RUNNING),
+            )
+
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                    )
+                )
+
+            sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
+
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
+
+            sendSteps(
+                TransitionStep(DOZING, GONE, 0f, STARTED),
+                TransitionStep(DOZING, GONE, 0f, RUNNING),
+            )
+
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                    )
+                )
+
+            sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+
+            assertThat(results)
+                .isEqualTo(
+                    listOf(
+                        false,
+                        true,
+                        false,
+                        true,
+                    )
+                )
+        }
+
+    @Test
+    fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() =
+        testScope.runTest {
+            val finishedStates by collectValues(underTest.finishedKeyguardState)
+
+            // We default FINISHED in LOCKSCREEN.
+            assertEquals(listOf(LOCKSCREEN), finishedStates)
+
+            sendSteps(
                 TransitionStep(LOCKSCREEN, AOD, 0f, STARTED),
                 TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING),
                 TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED),
-        )
+            )
 
-        // We're FINISHED in AOD.
-        assertEquals(listOf(
-                LOCKSCREEN,
-                AOD,
-        ), finishedStates)
+            // We're FINISHED in AOD.
+            assertEquals(
+                listOf(
+                    LOCKSCREEN,
+                    AOD,
+                ),
+                finishedStates
+            )
 
-        // Transition back to LOCKSCREEN.
-        sendSteps(
+            // Transition back to LOCKSCREEN.
+            sendSteps(
                 TransitionStep(AOD, LOCKSCREEN, 0f, STARTED),
                 TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING),
                 TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED),
-        )
+            )
 
-        // We're FINISHED in LOCKSCREEN.
-        assertEquals(listOf(
-                LOCKSCREEN,
-                AOD,
-                LOCKSCREEN,
-        ), finishedStates)
+            // We're FINISHED in LOCKSCREEN.
+            assertEquals(
+                listOf(
+                    LOCKSCREEN,
+                    AOD,
+                    LOCKSCREEN,
+                ),
+                finishedStates
+            )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
                 TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
-        )
+            )
 
-        // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in
-        // LOCKSCREEN.
-        assertEquals(listOf(
-                LOCKSCREEN,
-                AOD,
-                LOCKSCREEN,
-        ), finishedStates)
+            // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in
+            // LOCKSCREEN.
+            assertEquals(
+                listOf(
+                    LOCKSCREEN,
+                    AOD,
+                    LOCKSCREEN,
+                ),
+                finishedStates
+            )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(LOCKSCREEN, GONE, 0.6f, CANCELED),
-        )
+            )
 
-        // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN.
-        assertEquals(listOf(
-                LOCKSCREEN,
-                AOD,
-                LOCKSCREEN,
-        ), finishedStates)
+            // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN.
+            assertEquals(
+                listOf(
+                    LOCKSCREEN,
+                    AOD,
+                    LOCKSCREEN,
+                ),
+                finishedStates
+            )
 
-        sendSteps(
+            sendSteps(
                 TransitionStep(GONE, LOCKSCREEN, 0.6f, STARTED),
                 TransitionStep(GONE, LOCKSCREEN, 0.9f, RUNNING),
                 TransitionStep(GONE, LOCKSCREEN, 1f, FINISHED),
-        )
+            )
 
-        // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to
-        // LOCKSCREEN after the cancellation.
-        assertEquals(listOf(
-                LOCKSCREEN,
-                AOD,
-                LOCKSCREEN,
-                LOCKSCREEN,
-        ), finishedStates)
-    }
+            // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to
+            // LOCKSCREEN after the cancellation.
+            assertEquals(
+                listOf(
+                    LOCKSCREEN,
+                    AOD,
+                    LOCKSCREEN,
+                    LOCKSCREEN,
+                ),
+                finishedStates
+            )
+        }
+
+    @Test
+    fun testCurrentState() =
+        testScope.runTest {
+            val currentStates by collectValues(underTest.currentKeyguardState)
+
+            // We init the repo with a transition from OFF -> LOCKSCREEN.
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, AOD, 0f, STARTED),
+            )
+
+            // The current state should continue to be LOCKSCREEN as we transition to AOD.
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING),
+            )
+
+            // The current state should continue to be LOCKSCREEN as we transition to AOD.
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, AOD, 0.6f, CANCELED),
+            )
+
+            // Once CANCELED, we're still currently in LOCKSCREEN...
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(AOD, LOCKSCREEN, 0.6f, STARTED),
+            )
+
+            // ...until STARTING back to LOCKSCREEN, at which point the "current" state should be
+            // the
+            // one we're transitioning from, despite never FINISHING in that state.
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                    AOD,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(AOD, LOCKSCREEN, 0.8f, RUNNING),
+                TransitionStep(AOD, LOCKSCREEN, 0.8f, FINISHED),
+            )
+
+            // FINSHING in LOCKSCREEN should update the current state to LOCKSCREEN.
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                    AOD,
+                    LOCKSCREEN,
+                ),
+                currentStates
+            )
+        }
+
+    @Test
+    fun testCurrentState_multipleCancellations_backToLastFinishedState() =
+        testScope.runTest {
+            val currentStates by collectValues(underTest.currentKeyguardState)
+
+            // We init the repo with a transition from OFF -> LOCKSCREEN.
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+                TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+                TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+            )
+
+            assertEquals(
+                listOf(
+                    // Default transition from OFF -> LOCKSCREEN
+                    OFF,
+                    LOCKSCREEN,
+                    // Transitioned to GONE
+                    GONE,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(GONE, DOZING, 0f, STARTED),
+                TransitionStep(GONE, DOZING, 0.5f, RUNNING),
+                TransitionStep(GONE, DOZING, 0.6f, CANCELED),
+            )
+
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                    GONE,
+                    // Current state should not be DOZING until the post-cancelation transition is
+                    // STARTED
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
+            )
+
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                    GONE,
+                    // DOZING -> LS STARTED, DOZING is now the current state.
+                    DOZING,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING),
+                TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED),
+            )
+
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                    GONE,
+                    DOZING,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+            )
+
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                    GONE,
+                    DOZING,
+                    // LS -> GONE STARTED, LS is now the current state.
+                    LOCKSCREEN,
+                ),
+                currentStates
+            )
+
+            sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+                TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+            )
+
+            assertEquals(
+                listOf(
+                    OFF,
+                    LOCKSCREEN,
+                    GONE,
+                    DOZING,
+                    LOCKSCREEN,
+                    // FINISHED in GONE, GONE is now the current state.
+                    GONE,
+                ),
+                currentStates
+            )
+        }
 
     private suspend fun sendSteps(vararg steps: TransitionStep) {
         steps.forEach {
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 5b88ebe6..fd2fd2f 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
@@ -203,4 +203,38 @@
 
             assertThat(isVisible?.isAnimating).isEqualTo(false)
         }
+
+    @Test
+    fun alpha_glanceableHubOpen_isZero() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.alpha)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope,
+            )
+
+            assertThat(alpha).isEqualTo(0f)
+        }
+
+    @Test
+    fun alpha_glanceableHubClosed_isOne() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.alpha)
+
+            // Transition to the glanceable hub and back.
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope,
+            )
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.LOCKSCREEN,
+                testScope,
+            )
+
+            assertThat(alpha).isEqualTo(1.0f)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
new file mode 100644
index 0000000..d4dd2ac
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.authController
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenContentViewModelTest : SysuiTestCase() {
+
+    private val kosmos: Kosmos = testKosmos()
+
+    lateinit var underTest: LockscreenContentViewModel
+
+    @Before
+    fun setup() {
+        with(kosmos) {
+            fakeFeatureFlagsClassic.set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, true)
+            underTest = lockscreenContentViewModel
+        }
+    }
+
+    @Test
+    fun isUdfpsVisible_withUdfps_true() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(kosmos.authController.isUdfpsSupported).thenReturn(true)
+                assertThat(underTest.isUdfpsVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun isUdfpsVisible_withoutUdfps_false() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(kosmos.authController.isUdfpsSupported).thenReturn(false)
+                assertThat(underTest.isUdfpsVisible).isFalse()
+            }
+        }
+
+    @Test
+    fun isLargeClockVisible_withLargeClock_true() =
+        with(kosmos) {
+            testScope.runTest {
+                kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
+                assertThat(underTest.isLargeClockVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun isLargeClockVisible_withSmallClock_false() =
+        with(kosmos) {
+            testScope.runTest {
+                kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL)
+                assertThat(underTest.isLargeClockVisible).isFalse()
+            }
+        }
+
+    @Test
+    fun areNotificationsVisible_withSmallClock_true() =
+        with(kosmos) {
+            testScope.runTest {
+                kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL)
+                assertThat(underTest.areNotificationsVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun areNotificationsVisible_withLargeClock_false() =
+        with(kosmos) {
+            testScope.runTest {
+                kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
+                assertThat(underTest.areNotificationsVisible).isFalse()
+            }
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 74d309c..04e90c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -87,11 +87,7 @@
     private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel {
         return LockscreenSceneViewModel(
             applicationScope = testScope.backgroundScope,
-            deviceEntryInteractor =
-                utils.deviceEntryInteractor(
-                    authenticationInteractor = utils.authenticationInteractor(),
-                    sceneInteractor = utils.sceneInteractor(),
-                ),
+            deviceEntryInteractor = utils.deviceEntryInteractor(),
             communalInteractor = utils.communalInteractor(),
             longPress =
                 KeyguardLongPressViewModel(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index 070e07a..eb845b2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -17,11 +17,9 @@
 package com.android.systemui.qs.pipeline.data.repository
 
 import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE
-import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ResolveInfoFlags
@@ -33,44 +31,36 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.fakePackageChangeRepository
+import com.android.systemui.common.data.repository.packageChangeRepository
+import com.android.systemui.common.data.shared.model.PackageChangeModel
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argThat
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
-@OptIn(ExperimentalCoroutinesApi::class)
 class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
-    private val testDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     @Mock private lateinit var context: Context
     @Mock private lateinit var packageManager: PackageManager
-    @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
 
     private lateinit var underTest: InstalledTilesComponentRepositoryImpl
 
@@ -92,63 +82,12 @@
         underTest =
             InstalledTilesComponentRepositoryImpl(
                 context,
-                testDispatcher,
+                kosmos.testDispatcher,
+                kosmos.packageChangeRepository
             )
     }
 
     @Test
-    fun registersAndUnregistersBroadcastReceiver() =
-        testScope.runTest {
-            val user = 10
-            val job = launch { underTest.getInstalledTilesComponents(user).collect {} }
-            runCurrent()
-
-            verify(context)
-                .registerReceiverAsUser(
-                    capture(receiverCaptor),
-                    eq(UserHandle.of(user)),
-                    any(),
-                    nullable(),
-                    nullable(),
-                )
-
-            verify(context, never()).unregisterReceiver(receiverCaptor.value)
-
-            job.cancel()
-            runCurrent()
-            verify(context).unregisterReceiver(receiverCaptor.value)
-        }
-
-    @Test
-    fun intentFilterForCorrectActionsAndScheme() =
-        testScope.runTest {
-            val filterCaptor = argumentCaptor<IntentFilter>()
-
-            backgroundScope.launch { underTest.getInstalledTilesComponents(0).collect {} }
-            runCurrent()
-
-            verify(context)
-                .registerReceiverAsUser(
-                    any(),
-                    any(),
-                    capture(filterCaptor),
-                    nullable(),
-                    nullable(),
-                )
-
-            with(filterCaptor.value) {
-                assertThat(matchAction(Intent.ACTION_PACKAGE_CHANGED)).isTrue()
-                assertThat(matchAction(Intent.ACTION_PACKAGE_ADDED)).isTrue()
-                assertThat(matchAction(Intent.ACTION_PACKAGE_REMOVED)).isTrue()
-                assertThat(matchAction(Intent.ACTION_PACKAGE_REPLACED)).isTrue()
-                assertThat(countActions()).isEqualTo(4)
-
-                assertThat(hasDataScheme("package")).isTrue()
-                assertThat(countDataSchemes()).isEqualTo(1)
-            }
-        }
-
-    @Test
     fun componentsLoadedOnStart() =
         testScope.runTest {
             val userId = 0
@@ -169,7 +108,7 @@
         }
 
     @Test
-    fun componentAdded_foundAfterBroadcast() =
+    fun componentAdded_foundAfterPackageChange() =
         testScope.runTest {
             val userId = 0
             val resolveInfo =
@@ -186,7 +125,7 @@
                     )
                 )
                 .thenReturn(listOf(resolveInfo))
-            getRegisteredReceiver().onReceive(context, Intent(Intent.ACTION_PACKAGE_ADDED))
+            kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty)
 
             assertThat(componentNames).containsExactly(TEST_COMPONENT)
         }
@@ -275,19 +214,6 @@
             assertThat(componentNames).containsExactly(TEST_COMPONENT)
         }
 
-    private fun getRegisteredReceiver(): BroadcastReceiver {
-        verify(context)
-            .registerReceiverAsUser(
-                capture(receiverCaptor),
-                any(),
-                any(),
-                nullable(),
-                nullable(),
-            )
-
-        return receiverCaptor.value
-    }
-
     companion object {
         private val INTENT = Intent(TileService.ACTION_QS_TILE)
         private val FLAGS =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 530d127d..ecc2ef1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -24,6 +24,7 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.R
 import com.android.internal.util.EmergencyAffordanceManager
+import com.android.internal.util.emergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -35,13 +36,11 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
-import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.model.SysUiState
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.shared.model.ObservableTransitionState
@@ -61,6 +60,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.telecom.telecomManager
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -101,27 +101,12 @@
 @RunWith(AndroidJUnit4::class)
 class SceneFrameworkIntegrationTest : SysuiTestCase() {
 
-    @Mock private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
-    @Mock private lateinit var tableLogger: TableLogBuffer
-    @Mock private lateinit var telecomManager: TelecomManager
-
-    private val utils = SceneTestUtils(this)
+    private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true }
     private val testScope = utils.testScope
     private val sceneContainerConfig = utils.fakeSceneContainerConfig()
-    private val sceneRepository =
-        utils.fakeSceneContainerRepository(
-            containerConfig = sceneContainerConfig,
-        )
-    private val sceneInteractor =
-        utils.sceneInteractor(
-            repository = sceneRepository,
-        )
+    private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-        )
+    private val deviceEntryInteractor = utils.deviceEntryInteractor()
     private val communalInteractor = utils.communalInteractor()
 
     private val transitionState =
@@ -135,10 +120,7 @@
             )
             .apply { setTransitionState(transitionState) }
 
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
 
     private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
     private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor
@@ -170,19 +152,15 @@
                     FakeMobileConnectionsRepository(),
                 ),
             constants = mock(),
-            utils.featureFlags,
+            utils.fakeFeatureFlags,
             scope = testScope.backgroundScope,
         )
 
     private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
     private lateinit var shadeSceneViewModel: ShadeSceneViewModel
 
-    private val keyguardRepository = utils.keyguardRepository
-    private val keyguardInteractor =
-        utils.keyguardInteractor(
-            repository = keyguardRepository,
-        )
-    private val powerInteractor = PowerInteractorFactory.create().powerInteractor
+    private val keyguardInteractor = utils.keyguardInteractor()
+    private val powerInteractor = utils.powerInteractor()
 
     private var bouncerSceneJob: Job? = null
 
@@ -191,21 +169,26 @@
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var mediaHost: MediaHost
 
+    private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
+    private lateinit var telecomManager: TelecomManager
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+
         overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, true)
+        telecomManager = checkNotNull(utils.kosmos.telecomManager)
         whenever(telecomManager.isInCall).thenReturn(false)
+        emergencyAffordanceManager = utils.kosmos.emergencyAffordanceManager
         whenever(emergencyAffordanceManager.needsEmergencyAffordance()).thenReturn(true)
 
-        utils.featureFlags.apply {
+        utils.fakeFeatureFlags.apply {
             set(Flags.NEW_NETWORK_SLICE_UI, false)
             set(Flags.REFACTOR_GETCURRENTUSER, true)
         }
 
-        mobileConnectionsRepository =
-            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
-        mobileConnectionsRepository.isAnySimSecure.value = true
+        mobileConnectionsRepository = utils.mobileConnectionsRepository
+        mobileConnectionsRepository.isAnySimSecure.value = false
 
         utils.telephonyRepository.apply {
             setHasTelephonyRadio(true)
@@ -213,18 +196,8 @@
             setIsInCall(false)
         }
 
-        bouncerActionButtonInteractor =
-            utils.bouncerActionButtonInteractor(
-                mobileConnectionsRepository = mobileConnectionsRepository,
-                telecomManager = telecomManager,
-                emergencyAffordanceManager = emergencyAffordanceManager,
-            )
-        bouncerViewModel =
-            utils.bouncerViewModel(
-                bouncerInteractor = bouncerInteractor,
-                authenticationInteractor = authenticationInteractor,
-                actionButtonInteractor = bouncerActionButtonInteractor,
-            )
+        bouncerActionButtonInteractor = utils.bouncerActionButtonInteractor()
+        bouncerViewModel = utils.bouncerViewModel()
 
         shadeHeaderViewModel =
             ShadeHeaderViewModel(
@@ -257,7 +230,7 @@
                 sceneInteractor = sceneInteractor,
                 deviceEntryInteractor = deviceEntryInteractor,
                 keyguardInteractor = keyguardInteractor,
-                flags = utils.sceneContainerFlags,
+                flags = utils.fakeSceneContainerFlags,
                 sysUiState = sysUiState,
                 displayId = displayTracker.defaultDisplayId,
                 sceneLogger = mock(),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
index ddeb05b..339d026 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
@@ -39,7 +39,7 @@
 @RunWith(AndroidJUnit4::class)
 class SceneContainerRepositoryTest : SysuiTestCase() {
 
-    private val utils = SceneTestUtils(this)
+    private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true }
     private val testScope = utils.testScope
 
     @Test
@@ -72,7 +72,7 @@
     @Test(expected = IllegalStateException::class)
     fun setDesiredScene_noSuchSceneInContainer_throws() {
         utils.kosmos.sceneKeys = listOf(SceneKey.QuickSettings, SceneKey.Lockscreen)
-        val underTest = utils.fakeSceneContainerRepository(utils.fakeSceneContainerConfig())
+        val underTest = utils.fakeSceneContainerRepository()
         underTest.setDesiredScene(SceneModel(SceneKey.Shade))
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 7f4bbbe..486f7ab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -28,6 +28,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,8 +38,14 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
-    private val repository = utils.fakeSceneContainerRepository()
-    private val underTest = utils.sceneInteractor(repository = repository)
+
+    private lateinit var underTest: SceneInteractor
+
+    @Before
+    fun setUp() {
+        utils.fakeSceneContainerFlags.enabled = true
+        underTest = utils.sceneInteractor()
+    }
 
     @Test
     fun allSceneKeys() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index dd22976..5fe4ca1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -28,7 +28,7 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.model.SysUiState
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
@@ -68,17 +68,11 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
-    private val sceneContainerFlags = utils.sceneContainerFlags
+    private val sceneContainerFlags = utils.fakeSceneContainerFlags
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(authenticationInteractor = authenticationInteractor)
-    private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            faceAuthRepository = faceAuthRepository,
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private val faceAuthRepository = utils.kosmos.fakeDeviceEntryFaceAuthRepository
+    private val deviceEntryInteractor = utils.deviceEntryInteractor()
     private val keyguardInteractor = utils.keyguardInteractor()
     private val sysUiState: SysUiState = mock()
     private val falsingCollector: FalsingCollector = mock()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index c89cd9e..3a4ee64 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -28,6 +28,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,11 +38,17 @@
 
     private val utils = SceneTestUtils(this)
     private val interactor = utils.sceneInteractor()
-    private val underTest =
-        SceneContainerViewModel(
-            sceneInteractor = interactor,
-            falsingInteractor = utils.falsingInteractor(),
-        )
+    private lateinit var underTest: SceneContainerViewModel
+
+    @Before
+    fun setUp() {
+        utils.fakeSceneContainerFlags.enabled = true
+        underTest =
+            SceneContainerViewModel(
+                sceneInteractor = interactor,
+                falsingInteractor = utils.falsingInteractor(),
+            )
+    }
 
     @Test
     fun isVisible() = runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 1d2497d..a8133a3a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -56,12 +56,7 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
-    private val authenticationInteractor = utils.authenticationInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-        )
+    private val deviceEntryInteractor = utils.deviceEntryInteractor()
 
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
     private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 4cdb08a..607996d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -27,8 +27,7 @@
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
-import com.android.systemui.scene.shared.flag.sceneContainerFlags
+import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -50,7 +49,7 @@
 
     private val kosmos =
         testKosmos().apply {
-            sceneContainerFlags = FakeSceneContainerFlags(enabled = true)
+            fakeSceneContainerFlags.enabled = true
             fakeFeatureFlagsClassic.apply {
                 set(Flags.FULL_SCREEN_USER_SWITCHER, false)
                 set(Flags.NSSL_DEBUG_LINES, false)
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml
new file mode 100644
index 0000000..045c19e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml
new file mode 100644
index 0000000..5e012ab
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillColor="#fff"/>
+</vector>
+
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml
new file mode 100644
index 0000000..d8a9a70
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
new file mode 100644
index 0000000..dec9930
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    >
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/bindable_status_bar_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml
new file mode 100644
index 0000000..ee4d05c
--- /dev/null
+++ b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<!-- Base layout that provides a single bindable icon_view id image view -->
+<com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    >
+
+    <ImageView
+        android:id="@+id/icon_view"
+        android:layout_height="@dimen/status_bar_bindable_icon_size"
+        android:layout_width="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:padding="@dimen/status_bar_bindable_icon_padding"
+        android:scaleType="fitCenter"
+        />
+
+</com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView>
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml
new file mode 100644
index 0000000..3908757
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/customized_view"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:orientation="vertical"
+    style="@style/AuthCredentialContentLayoutStyle">
+
+    <TextView
+        android:id="@+id/customized_view_title"
+        style="@style/TextAppearance.AuthCredential.ContentViewTitle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:ellipsize="marquee"
+        android:marqueeRepeatLimit="1"
+        android:singleLine="true" />
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml
new file mode 100644
index 0000000..e39f60f
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/TextAppearance.AuthCredential.ContentViewListItem"
+    android:layout_width="0dp"
+    android:layout_height="match_parent"
+    android:layout_weight="1.0" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml
new file mode 100644
index 0000000..6c86736
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/biometric_prompt_content_list_row_height"
+    android:gravity="center_vertical|start"
+    android:orientation="horizontal" />
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
index bea0e13..23fbb12 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -48,6 +48,23 @@
         android:importantForAccessibility="no"
         style="@style/TextAppearance.AuthCredential.Description"/>
 
+    <Space
+        android:id="@+id/space_above_content"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/biometric_prompt_space_above_content"
+        android:visibility="gone" />
+
+    <ScrollView
+        android:id="@+id/customized_view_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fadeScrollbars="false"
+        android:gravity="center_vertical"
+        android:orientation="vertical"
+        android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal"
+        android:scrollbars="vertical"
+        android:visibility="gone" />
+
     <Space android:id="@+id/space_above_icon"
         android:layout_width="match_parent"
         android:layout_height="48dp" />
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index cfb4017..55606aa 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -86,8 +86,8 @@
     <!-- Power Menu Lite -->
     <!-- These values are for small screen landscape. For larger landscape screens, they are
          overlaid -->
-    <dimen name="global_actions_button_size">72dp</dimen>
-    <dimen name="global_actions_button_padding">26dp</dimen>
+    <dimen name="global_actions_button_size">64dp</dimen>
+    <dimen name="global_actions_button_padding">20dp</dimen>
 
     <!-- scroll view the size of 2 channel rows -->
     <dimen name="notification_blocker_channel_list_height">128dp</dimen>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 5f6a39a..8be1cc7 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -135,6 +135,9 @@
     <color name="biometric_dialog_gray">#ff757575</color>
     <color name="biometric_dialog_accent">@color/material_dynamic_primary40</color>
     <color name="biometric_dialog_error">#ffd93025</color>                  <!-- red 600 -->
+    <!-- Color for biometric prompt content view -->
+    <color name="biometric_prompt_content_background_color">#8AB4F8</color>
+    <color name="biometric_prompt_content_list_item_bullet_color">#1d873b</color>
 
     <!-- SFPS colors -->
     <color name="sfps_chevron_fill">@color/material_dynamic_primary90</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 90d8cdb..9a4520c 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -151,6 +151,8 @@
     <dimen name="status_bar_icon_size_sp">@*android:dimen/status_bar_icon_size_sp</dimen>
     <!-- Original dp height of notification icons in the status bar -->
     <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen>
+    <dimen name="status_bar_bindable_icon_size">20sp</dimen>
+    <dimen name="status_bar_bindable_icon_padding">2sp</dimen>
 
     <!-- Default horizontal drawable padding for status bar icons. -->
     <dimen name="status_bar_horizontal_padding">2.5sp</dimen>
@@ -1092,6 +1094,16 @@
     <dimen name="biometric_dialog_width">240dp</dimen>
     <dimen name="biometric_dialog_height">240dp</dimen>
 
+    <!-- Dimensions for biometric prompt content view. -->
+    <dimen name="biometric_prompt_space_above_content">48dp</dimen>
+    <dimen name="biometric_prompt_content_container_padding_horizontal">24dp</dimen>
+    <dimen name="biometric_prompt_content_padding_horizontal">10dp</dimen>
+    <dimen name="biometric_prompt_content_list_row_height">24dp</dimen>
+    <dimen name="biometric_prompt_content_list_item_padding_horizontal">10dp</dimen>
+    <dimen name="biometric_prompt_content_list_item_text_size">14sp</dimen>
+    <dimen name="biometric_prompt_content_list_item_bullet_gap_width">10dp</dimen>
+    <dimen name="biometric_prompt_content_list_item_bullet_radius">5dp</dimen>
+
     <!-- Biometric Auth Credential values -->
     <dimen name="biometric_auth_icon_size">48dp</dimen>
 
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index c925b26..fad4d4f 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -16,6 +16,7 @@
   -->
 <resources>
     <integer name="biometric_dialog_text_gravity">8388611</integer> <!-- gravity start -->
+    <integer name="biometric_prompt_content_list_item_max_lines_if_two_column">3</integer>
 
     <integer name="qs_security_footer_maxLines">2</integer>
 
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7d7c050..ab3cacb 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -195,6 +195,24 @@
         <item name="android:textSize">14sp</item>
     </style>
 
+    <style name="TextAppearance.AuthCredential.ContentViewTitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:paddingTop">8dp</item>
+        <item name="android:paddingHorizontal">24dp</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:gravity">start</item>
+    </style>
+
+    <style name="TextAppearance.AuthCredential.ContentViewListItem">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:paddingTop">8dp</item>
+        <item name="android:paddingHorizontal">
+            @dimen/biometric_prompt_content_list_item_padding_horizontal
+        </item>
+        <item name="android:textSize">@dimen/biometric_prompt_content_list_item_text_size</item>
+        <item name="android:gravity">start</item>
+    </style>
+
     <style name="TextAppearance.AuthCredential.Error">
         <item name="android:paddingTop">6dp</item>
         <item name="android:paddingHorizontal">24dp</item>
@@ -294,6 +312,11 @@
         <item name="android:textSize">16sp</item>
     </style>
 
+    <style name="AuthCredentialContentLayoutStyle">
+        <item name="android:background">@color/biometric_prompt_content_background_color</item>
+        <item name="android:paddingHorizontal">@dimen/biometric_prompt_content_padding_horizontal</item>
+    </style>
+
     <style name="DeviceManagementDialogTitle">
         <item name="android:gravity">center</item>
         <item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index a5a545a..033f93b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -3,6 +3,7 @@
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN;
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN;
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE;
+import static com.android.systemui.Flags.migrateClocksToBlueprint;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -191,11 +192,11 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-
-        mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
-        mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
-        mStatusArea = findViewById(R.id.keyguard_status_area);
-
+        if (!migrateClocksToBlueprint()) {
+            mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
+            mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
+            mStatusArea = findViewById(R.id.keyguard_status_area);
+        }
         onConfigChanged();
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index d372f5a..1758831 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -494,7 +494,7 @@
             boolean shouldBeCentered,
             boolean animate) {
         if (migrateClocksToBlueprint()) {
-            mKeyguardInteractor.setClockShouldBeCentered(mSplitShadeEnabled && shouldBeCentered);
+            mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
         } else {
             mKeyguardClockSwitchController.setSplitShadeCentered(
                     splitShadeEnabled && shouldBeCentered);
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt
similarity index 79%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt
index ce92b6d..9f54c26 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package com.android.systemui
+
+/** A [CoreStartable] that does nothing. */
+class NoOpCoreStartable : CoreStartable {
+    override fun start() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 7d9ec08..f5603ed 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -23,6 +23,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
 import com.android.systemui.Dumpable
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -49,7 +50,8 @@
     protected val statusBarStateController: StatusBarStateController,
     protected val shadeInteractor: ShadeInteractor,
     protected val dialogManager: SystemUIDialogManager,
-    private val dumpManager: DumpManager
+    private val dumpManager: DumpManager,
+    private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) : ViewController<T>(view), Dumpable {
 
     protected abstract val tag: String
@@ -130,11 +132,13 @@
     override fun onViewAttached() {
         dialogManager.registerListener(dialogListener)
         dumpManager.registerDumpable(dumpTag, this)
+        udfpsOverlayInteractor.setHandleTouches(shouldHandle = true)
     }
 
     override fun onViewDetached() {
         dialogManager.unregisterListener(dialogListener)
         dumpManager.unregisterDumpable(dumpTag)
+        udfpsOverlayInteractor.setHandleTouches(shouldHandle = true)
     }
 
     /**
@@ -165,6 +169,7 @@
     fun updatePauseAuth() {
         if (view.setPauseAuth(shouldPauseAuth())) {
             view.postInvalidate()
+            udfpsOverlayInteractor.setHandleTouches(shouldHandle = !shouldPauseAuth())
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
index e7b0d9f..e0455b5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.biometrics
 
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -28,13 +29,15 @@
     statusBarStateController: StatusBarStateController,
     shadeInteractor: ShadeInteractor,
     systemUIDialogManager: SystemUIDialogManager,
-    dumpManager: DumpManager
+    dumpManager: DumpManager,
+    udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) : UdfpsAnimationViewController<UdfpsBpView>(
     view,
     statusBarStateController,
     shadeInteractor,
     systemUIDialogManager,
-    dumpManager
+    dumpManager,
+    udfpsOverlayInteractor,
 ) {
     override val tag = "UdfpsBpViewController"
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 81de0a2..66fe4b3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -68,6 +68,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.biometrics.dagger.BiometricsBackground;
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
 import com.android.systemui.biometrics.udfps.InteractionEvent;
 import com.android.systemui.biometrics.udfps.NormalizedTouchData;
@@ -171,6 +172,7 @@
     @NonNull private final Lazy<DefaultUdfpsTouchOverlayViewModel>
             mDefaultUdfpsTouchOverlayViewModel;
     @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor;
+    @NonNull private final UdfpsOverlayInteractor mUdfpsOverlayInteractor;
     @NonNull private final InputManager mInputManager;
     @NonNull private final UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     @NonNull private final SelectedUserInteractor mSelectedUserInteractor;
@@ -293,7 +295,8 @@
                         mSelectedUserInteractor,
                         mDeviceEntryUdfpsTouchOverlayViewModel,
                         mDefaultUdfpsTouchOverlayViewModel,
-                        mShadeInteractor
+                        mShadeInteractor,
+                        mUdfpsOverlayInteractor
                     )));
         }
 
@@ -674,7 +677,8 @@
             @NonNull FpsUnlockTracker fpsUnlockTracker,
             @NonNull KeyguardTransitionInteractor keyguardTransitionInteractor,
             Lazy<DeviceEntryUdfpsTouchOverlayViewModel> deviceEntryUdfpsTouchOverlayViewModel,
-            Lazy<DefaultUdfpsTouchOverlayViewModel> defaultUdfpsTouchOverlayViewModel) {
+            Lazy<DefaultUdfpsTouchOverlayViewModel> defaultUdfpsTouchOverlayViewModel,
+            @NonNull UdfpsOverlayInteractor udfpsOverlayInteractor) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -715,6 +719,7 @@
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
         mShadeInteractor = shadeInteractor;
         mAlternateBouncerInteractor = alternateBouncerInteractor;
+        mUdfpsOverlayInteractor = udfpsOverlayInteractor;
         mInputManager = inputManager;
         mUdfpsKeyguardAccessibilityDelegate = udfpsKeyguardAccessibilityDelegate;
         mSelectedUserInteractor = selectedUserInteractor;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index b94a177..4176083 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -45,6 +45,7 @@
 import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
 import com.android.systemui.biometrics.ui.binder.UdfpsTouchOverlayBinder
 import com.android.systemui.biometrics.ui.view.UdfpsTouchOverlay
@@ -109,6 +110,7 @@
     private val deviceEntryUdfpsTouchOverlayViewModel: Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
     private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
     private val shadeInteractor: ShadeInteractor,
+    private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) {
     private var overlayViewLegacy: UdfpsView? = null
         private set
@@ -281,7 +283,8 @@
                     statusBarStateController,
                     shadeInteractor,
                     dialogManager,
-                    dumpManager
+                    dumpManager,
+                    udfpsOverlayInteractor,
                 )
             }
             REASON_AUTH_KEYGUARD -> {
@@ -306,6 +309,7 @@
                     selectedUserInteractor,
                     transitionInteractor,
                     shadeInteractor,
+                    udfpsOverlayInteractor,
                 )
             }
             REASON_AUTH_BP -> {
@@ -315,7 +319,8 @@
                     statusBarStateController,
                     shadeInteractor,
                     dialogManager,
-                    dumpManager
+                    dumpManager,
+                    udfpsOverlayInteractor,
                 )
             }
             REASON_AUTH_OTHER,
@@ -325,7 +330,8 @@
                     statusBarStateController,
                     shadeInteractor,
                     dialogManager,
-                    dumpManager
+                    dumpManager,
+                    udfpsOverlayInteractor,
                 )
             }
             else -> {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
index abbfa01..86802a5b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
@@ -131,23 +131,21 @@
                 icon.measure(
                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
                         MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
-            } else if (child.getId() == R.id.space_above_icon) {
+            } else if (child.getId() == R.id.space_above_icon
+                    || child.getId() == R.id.space_above_content
+                    || child.getId() == R.id.button_bar) {
                 child.measure(
                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                         MeasureSpec.makeMeasureSpec(
                                 child.getLayoutParams().height, MeasureSpec.EXACTLY));
-            } else if (child.getId() == R.id.button_bar) {
-                child.measure(
-                        MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
-                        MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
-                                MeasureSpec.EXACTLY));
             } else if (child.getId() == R.id.space_below_icon) {
                 // Set the spacer height so the fingerprint icon is on the physical sensor area
                 final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0);
                 child.measure(
                         MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                         MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY));
-            } else if (child.getId() == R.id.description) {
+            } else if (child.getId() == R.id.description
+                    || child.getId() == R.id.customized_view_container) {
                 //skip description view and compute later
                 continue;
             } else {
@@ -161,27 +159,28 @@
             }
         }
 
-        //re-calculate the height of description
+        //re-calculate the height of body content
         View description = mView.findViewById(R.id.description);
+        View contentView = mView.findViewById(R.id.customized_view_container);
         if (description != null && description.getVisibility() != View.GONE) {
             totalHeight += measureDescription(description, displayHeight, width, totalHeight);
+        } else if (contentView != null && contentView.getVisibility() != View.GONE) {
+            totalHeight += measureDescription(contentView, displayHeight, width, totalHeight);
         }
 
         return new AuthDialog.LayoutParams(width, totalHeight);
     }
 
-    private int measureDescription(View description, int displayHeight, int currWidth,
+    private int measureDescription(View bodyContent, int displayHeight, int currWidth,
                                    int currHeight) {
-        //description view should be measured in AuthBiometricFingerprintView#onMeasureInternal
-        //so we could getMeasuredHeight in onMeasureInternalPortrait directly.
-        int newHeight = description.getMeasuredHeight() + currHeight;
+        int newHeight = bodyContent.getMeasuredHeight() + currHeight;
         int limit = (int) (displayHeight * 0.75);
         if (newHeight > limit) {
-            description.measure(
+            bodyContent.measure(
                     MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY),
                     MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY));
         }
-        return description.getMeasuredHeight();
+        return bodyContent.getMeasuredHeight();
     }
 
     @NonNull
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt
index ab3fbb1..cfbbc26 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.biometrics
 
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -30,13 +31,15 @@
     statusBarStateController: StatusBarStateController,
     shadeInteractor: ShadeInteractor,
     systemUIDialogManager: SystemUIDialogManager,
-    dumpManager: DumpManager
+    dumpManager: DumpManager,
+    udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) : UdfpsAnimationViewController<UdfpsFpmEmptyView>(
     view,
     statusBarStateController,
     shadeInteractor,
     systemUIDialogManager,
-    dumpManager
+    dumpManager,
+    udfpsOverlayInteractor,
 ) {
     override val tag = "UdfpsFpmOtherViewController"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index 9f17024..7020d05 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -27,6 +27,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATE_APPEAR_ON_SCREEN_OFF
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dump.DumpManager
@@ -75,6 +76,7 @@
     private val selectedUserInteractor: SelectedUserInteractor,
     private val transitionInteractor: KeyguardTransitionInteractor,
     shadeInteractor: ShadeInteractor,
+    udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) :
     UdfpsAnimationViewController<UdfpsKeyguardViewLegacy>(
         view,
@@ -82,6 +84,7 @@
         shadeInteractor,
         systemUIDialogManager,
         dumpManager,
+        udfpsOverlayInteractor,
     ) {
     private val uniqueIdentifier = this.toString()
     private var showingUdfpsBouncer = false
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
new file mode 100644
index 0000000..e6939f0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.biometrics.domain.interactor
+
+import android.content.Context
+import android.hardware.biometrics.SensorLocationInternal
+import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorLocation
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class FingerprintPropertyInteractor
+@Inject
+constructor(
+    @Application private val context: Context,
+    repository: FingerprintPropertyRepository,
+    configurationInteractor: ConfigurationInteractor,
+    displayStateInteractor: DisplayStateInteractor,
+) {
+    /**
+     * Devices with multiple physical displays use unique display ids to determine which sensor is
+     * on the active physical display. This value represents a unique physical display id.
+     */
+    private val uniqueDisplayId: Flow<String> =
+        displayStateInteractor.displayChanges
+            .map { context.display?.uniqueId }
+            .filterNotNull()
+            .distinctUntilChanged()
+
+    /**
+     * Sensor location for the:
+     * - current physical display
+     * - device's natural screen resolution
+     * - device's natural orientation
+     */
+    private val unscaledSensorLocation: Flow<SensorLocationInternal> =
+        combine(
+            repository.sensorLocations,
+            uniqueDisplayId,
+        ) { locations, displayId ->
+            // Devices without multiple physical displays do not use the display id as the key;
+            // instead, the key is an empty string.
+            locations.getOrDefault(
+                displayId,
+                locations.getOrDefault("", SensorLocationInternal.DEFAULT)
+            )
+        }
+
+    /**
+     * Sensor location for the:
+     * - current physical display
+     * - current screen resolution
+     * - device's natural orientation
+     */
+    val sensorLocation: Flow<SensorLocation> =
+        combine(
+            unscaledSensorLocation,
+            configurationInteractor.scaleForResolution,
+        ) { unscaledSensorLocation, scale ->
+            val sensorLocation =
+                SensorLocation(
+                    unscaledSensorLocation.sensorLocationX,
+                    unscaledSensorLocation.sensorLocationY,
+                    unscaledSensorLocation.sensorRadius,
+                )
+            sensorLocation.scale = scale
+            sensorLocation
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
index 863ba8d..70be0f2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt
@@ -63,6 +63,9 @@
     /** Current display state, defined as [AuthenticateOptions.DisplayState] */
     val displayState: Flow<Int>
 
+    /** If touches on the fingerprint sensor should be ignored by the HAL. */
+    val isHardwareIgnoringTouches: Flow<Boolean>
+
     /**
      * Add a permanent context listener.
      *
@@ -79,12 +82,11 @@
     @Application private val applicationScope: CoroutineScope,
     private val foldProvider: FoldStateProvider,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) : LogContextInteractor {
 
     init {
-        applicationScope.launch {
-            foldProvider.start()
-        }
+        applicationScope.launch { foldProvider.start() }
     }
 
     override val displayState =
@@ -102,6 +104,9 @@
             }
         }
 
+    override val isHardwareIgnoringTouches: Flow<Boolean> =
+        udfpsOverlayInteractor.shouldHandleTouches.map { shouldHandle -> !shouldHandle }
+
     override val isAod =
         displayState.map { it == AuthenticateOptions.DISPLAY_STATE_AOD }.distinctUntilChanged()
 
@@ -159,6 +164,12 @@
                 .catch { t -> Log.w(TAG, "failed to notify new display state", t) }
                 .launchIn(this)
 
+            isHardwareIgnoringTouches
+                .distinctUntilChanged()
+                .onEach { state -> listener.onHardwareIgnoreTouchesChanged(state) }
+                .catch { t -> Log.w(TAG, "failed to notify new set ignore state", t) }
+                .launchIn(this)
+
             listener.asBinder().linkToDeath({ cancel() }, 0)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
index f4a2811..4fc1b58 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
@@ -30,8 +30,10 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
@@ -56,6 +58,16 @@
         return isUdfpsEnrolled && isWithinOverlayBounds
     }
 
+    /** Sets whether Udfps overlay should handle touches */
+    fun setHandleTouches(shouldHandle: Boolean = true) {
+        _shouldHandleTouches.value = shouldHandle
+    }
+
+    private var _shouldHandleTouches = MutableStateFlow(true)
+
+    /** Whether Udfps overlay should handle touches */
+    val shouldHandleTouches: StateFlow<Boolean> = _shouldHandleTouches.asStateFlow()
+
     /** Returns the current udfpsOverlayParams */
     val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> =
         ConflatedCallbackFlow.conflatedCallbackFlow {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
index 8fbb250..4377937 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.biometrics.domain.model
 
+import android.hardware.biometrics.PromptContentView
 import android.hardware.biometrics.PromptInfo
 import com.android.systemui.biometrics.shared.model.BiometricModalities
 import com.android.systemui.biometrics.shared.model.BiometricUserInfo
@@ -34,6 +35,7 @@
             operationInfo = operationInfo,
             showEmergencyCallButton = info.isShowEmergencyCallButton
         ) {
+        val contentView: PromptContentView? = info.contentView
         val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
new file mode 100644
index 0000000..dddadbd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.shared.model
+
+/** Provides current sensor location information in the current screen resolution [scale]. */
+data class SensorLocation(
+    private val naturalCenterX: Int,
+    private val naturalCenterY: Int,
+    private val naturalRadius: Int
+) {
+    /**
+     * Scale to apply to the sensor location's natural parameters to support different screen
+     * resolutions.
+     */
+    var scale: Float = 1f
+
+    val centerX: Float
+        get() {
+            return naturalCenterX * scale
+        }
+    val centerY: Float
+        get() {
+            return naturalCenterY * scale
+        }
+    val radius: Float
+        get() {
+            return naturalRadius * scale
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
index 0d72b9c..60b454e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
@@ -101,6 +101,7 @@
             final View child = getChildAt(i);
 
             if (child.getId() == R.id.space_above_icon
+                    || child.getId() == R.id.space_above_content
                     || child.getId() == R.id.space_below_icon
                     || child.getId() == R.id.button_bar) {
                 child.measure(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
new file mode 100644
index 0000000..22b02da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.biometrics.ui.binder
+
+import android.content.Context
+import android.content.res.Resources
+import android.content.res.Resources.Theme
+import android.graphics.Paint
+import android.hardware.biometrics.PromptContentListItem
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptContentListItemPlainText
+import android.hardware.biometrics.PromptContentView
+import android.hardware.biometrics.PromptVerticalListContentView
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.BulletSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.ScrollView
+import android.widget.Space
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import kotlin.math.ceil
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+/** Sub-binder for [BiometricPromptLayout.customized_view_container]. */
+object BiometricCustomizedViewBinder {
+    fun bind(customizedViewContainer: ScrollView, spaceAbove: Space, viewModel: PromptViewModel) {
+        customizedViewContainer.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    val contentView: PromptContentView? = viewModel.contentView.first()
+
+                    if (contentView != null) {
+                        val context = customizedViewContainer.context
+                        customizedViewContainer.addView(contentView.toView(context))
+                        customizedViewContainer.visibility = View.VISIBLE
+                        spaceAbove.visibility = View.VISIBLE
+                    } else {
+                        customizedViewContainer.visibility = View.GONE
+                        spaceAbove.visibility = View.GONE
+                    }
+                }
+            }
+        }
+    }
+}
+
+private fun PromptContentView.toView(context: Context): View {
+    val resources = context.resources
+    val inflater = LayoutInflater.from(context)
+    when (this) {
+        is PromptVerticalListContentView -> {
+            val contentView =
+                inflater.inflate(R.layout.biometric_prompt_content_layout, null) as LinearLayout
+
+            val descriptionView = contentView.requireViewById<TextView>(R.id.customized_view_title)
+            if (!description.isNullOrEmpty()) {
+                descriptionView.text = description
+            } else {
+                descriptionView.visibility = View.GONE
+            }
+
+            // Show two column by default, once there is an item exceeding max lines, show single
+            // item instead.
+            val showTwoColumn = listItems.all { !it.doesExceedMaxLinesIfTwoColumn(resources) }
+            var currRowView = createNewRowLayout(inflater)
+            for (item in listItems) {
+                val itemView = item.toView(resources, inflater, context.theme)
+                currRowView.addView(itemView)
+
+                if (!showTwoColumn || currRowView.childCount == 2) {
+                    contentView.addView(currRowView)
+                    currRowView = createNewRowLayout(inflater)
+                }
+            }
+            if (currRowView.childCount > 0) {
+                contentView.addView(currRowView)
+            }
+
+            return contentView
+        }
+        else -> {
+            throw IllegalStateException("No such PromptContentView: $this")
+        }
+    }
+}
+
+private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout {
+    return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout
+}
+
+private fun PromptContentListItem.doesExceedMaxLinesIfTwoColumn(
+    resources: Resources,
+): Boolean {
+    val passedInText: CharSequence =
+        when (this) {
+            is PromptContentListItemPlainText -> text
+            is PromptContentListItemBulletedText -> text
+            else -> {
+                throw IllegalStateException("No such ListItem: $this")
+            }
+        }
+
+    when (this) {
+        is PromptContentListItemPlainText,
+        is PromptContentListItemBulletedText -> {
+            val dialogMargin =
+                resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding)
+            val halfDialogWidth =
+                Resources.getSystem().displayMetrics.widthPixels / 2 - dialogMargin
+            val containerPadding =
+                resources.getDimensionPixelSize(
+                    R.dimen.biometric_prompt_content_container_padding_horizontal
+                )
+            val contentPadding =
+                resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_padding_horizontal)
+            val listItemPadding = getListItemPadding(resources)
+            val maxWidth = halfDialogWidth - containerPadding - contentPadding - listItemPadding
+
+            val text = "$passedInText"
+            val textSize =
+                resources.getDimensionPixelSize(
+                    R.dimen.biometric_prompt_content_list_item_text_size
+                )
+            val paint = Paint()
+            paint.textSize = textSize.toFloat()
+
+            val maxLines =
+                resources.getInteger(
+                    R.integer.biometric_prompt_content_list_item_max_lines_if_two_column
+                )
+            val numLines = ceil(paint.measureText(text).toDouble() / maxWidth).toInt()
+            return numLines > maxLines
+        }
+        else -> {
+            throw IllegalStateException("No such ListItem: $this")
+        }
+    }
+}
+
+private fun PromptContentListItem.toView(
+    resources: Resources,
+    inflater: LayoutInflater,
+    theme: Theme,
+): TextView {
+    val textView =
+        inflater.inflate(R.layout.biometric_prompt_content_row_item_text_view, null) as TextView
+    val lp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
+    textView.layoutParams = lp
+
+    when (this) {
+        is PromptContentListItemPlainText -> {
+            textView.text = text
+        }
+        is PromptContentListItemBulletedText -> {
+            val bulletedText = SpannableString(text)
+            val span =
+                BulletSpan(
+                    getListItemBulletGapWidth(resources),
+                    getListItemBulletColor(resources, theme),
+                    getListItemBulletRadius(resources)
+                )
+            bulletedText.setSpan(span, 0 /* start */, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+            textView.text = bulletedText
+        }
+        else -> {
+            throw IllegalStateException("No such ListItem: $this")
+        }
+    }
+    return textView
+}
+
+private fun PromptContentListItem.getListItemPadding(resources: Resources): Int {
+    var listItemPadding =
+        resources.getDimensionPixelSize(
+            R.dimen.biometric_prompt_content_list_item_padding_horizontal
+        ) * 2
+    when (this) {
+        is PromptContentListItemPlainText -> {}
+        is PromptContentListItemBulletedText -> {
+            listItemPadding +=
+                getListItemBulletRadius(resources) * 2 + getListItemBulletGapWidth(resources)
+        }
+        else -> {
+            throw IllegalStateException("No such ListItem: $this")
+        }
+    }
+    return listItemPadding
+}
+
+private fun getListItemBulletRadius(resources: Resources): Int =
+    resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_radius)
+
+private fun getListItemBulletGapWidth(resources: Resources): Int =
+    resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_gap_width)
+
+private fun getListItemBulletColor(resources: Resources, theme: Theme): Int =
+    resources.getColor(R.color.biometric_prompt_content_list_item_bullet_color, theme)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 7b8cb82..04dc7a8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -31,6 +31,7 @@
 import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
 import android.view.accessibility.AccessibilityManager
 import android.widget.Button
+import android.widget.ScrollView
 import android.widget.TextView
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.Lifecycle
@@ -94,6 +95,8 @@
         val titleView = view.requireViewById<TextView>(R.id.title)
         val subtitleView = view.requireViewById<TextView>(R.id.subtitle)
         val descriptionView = view.requireViewById<TextView>(R.id.description)
+        val customizedViewContainer =
+            view.requireViewById<ScrollView>(R.id.customized_view_container)
 
         // set selected to enable marquee unless a screen reader is enabled
         titleView.isSelected =
@@ -150,8 +153,13 @@
             }
 
             titleView.text = viewModel.title.first()
-            descriptionView.text = viewModel.description.first()
             subtitleView.text = viewModel.subtitle.first()
+            descriptionView.text = viewModel.description.first()
+            BiometricCustomizedViewBinder.bind(
+                customizedViewContainer,
+                view.requireViewById(R.id.space_above_content),
+                viewModel
+            )
 
             // set button listeners
             negativeButton.setOnClickListener { legacyCallback.onButtonNegative() }
@@ -178,12 +186,14 @@
                             titleView,
                             subtitleView,
                             descriptionView,
+                            customizedViewContainer,
                         ),
                     viewsToFadeInOnSizeChange =
                         listOf(
                             titleView,
                             subtitleView,
                             descriptionView,
+                            customizedViewContainer,
                             indicatorMessageView,
                             negativeButton,
                             cancelButton,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index 1a7b6c9..c3bbaed 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -55,7 +55,7 @@
     fun bind(
         view: BiometricPromptLayout,
         viewModel: PromptViewModel,
-        viewsToHideWhenSmall: List<TextView>,
+        viewsToHideWhenSmall: List<View>,
         viewsToFadeInOnSizeChange: List<View>,
         panelViewController: AuthPanelController,
         jankListener: BiometricJankListener,
@@ -110,7 +110,7 @@
 
                         // prepare for animated size transitions
                         for (v in viewsToHideWhenSmall) {
-                            v.showTextOrHide(forceHide = size.isSmall)
+                            v.showContentOrHide(forceHide = size.isSmall)
                         }
                         if (currentSize == null && size.isSmall) {
                             iconHolderView.alpha = 0f
@@ -119,6 +119,10 @@
                             viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
                         }
 
+                        // TODO(b/302735104): Fix wrong height due to the delay of
+                        // PromptContentView. addOnLayoutChangeListener() will cause crash when
+                        // showing credential view, since |PromptIconViewModel| won't release the
+                        // flow.
                         // propagate size changes to legacy panel controller and animate transitions
                         view.doOnLayout {
                             val width = view.measuredWidth
@@ -228,8 +232,9 @@
     return r == Surface.ROTATION_90 || r == Surface.ROTATION_270
 }
 
-private fun TextView.showTextOrHide(forceHide: Boolean = false) {
-    visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE
+private fun View.showContentOrHide(forceHide: Boolean = false) {
+    val isTextViewWithBlankText = this is TextView && this.text.isBlank()
+    visibility = if (forceHide || isTextViewWithBlankText) View.GONE else View.VISIBLE
 }
 
 private fun View.asVerticalAnimator(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index d899827e..1c78928 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -13,11 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.systemui.biometrics.ui.viewmodel
 
 import android.content.Context
 import android.graphics.Rect
 import android.hardware.biometrics.BiometricPrompt
+import android.hardware.biometrics.PromptContentView
 import android.util.Log
 import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
@@ -239,9 +241,20 @@
     val subtitle: Flow<String> =
         promptSelectorInteractor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
 
-    /** Description for the prompt. */
-    val description: Flow<String> =
+    /** Custom content view for the prompt. */
+    val contentView: Flow<PromptContentView?> =
+        promptSelectorInteractor.prompt.map { it?.contentView }.distinctUntilChanged()
+
+    private val originalDescription =
         promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
+    /**
+     * Description for the prompt. Description view and contentView is mutually exclusive. Pass
+     * description down only when contentView is null.
+     */
+    val description: Flow<String> =
+        combine(contentView, originalDescription) { contentView, description ->
+            if (contentView == null) description else ""
+        }
 
     /** If the indicator (help, error) message should be shown. */
     val isIndicatorMessageVisible: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
index c2a1d8f..d0ff185 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.bouncer.shared.model.BouncerDismissActionModel
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -90,6 +91,9 @@
     val alternateBouncerUIAvailable: StateFlow<Boolean>
     val sideFpsShowing: StateFlow<Boolean>
 
+    /** Action that should be run right after the bouncer is dismissed. */
+    var bouncerDismissActionModel: BouncerDismissActionModel?
+
     var lastAlternateBouncerVisibleTime: Long
 
     fun setPrimaryScrimmed(isScrimmed: Boolean)
@@ -134,6 +138,8 @@
     @Application private val applicationScope: CoroutineScope,
     @BouncerTableLog private val buffer: TableLogBuffer,
 ) : KeyguardBouncerRepository {
+    override var bouncerDismissActionModel: BouncerDismissActionModel? = null
+
     /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
     private val _primaryBouncerShow = MutableStateFlow(false)
     override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index 654fa22..8c87b0c 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -28,10 +28,12 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.DejankUtils
+import com.android.systemui.Flags
 import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.bouncer.shared.model.BouncerDismissActionModel
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.bouncer.ui.BouncerView
 import com.android.systemui.classifier.FalsingCollector
@@ -154,12 +156,12 @@
     /** Show the bouncer if necessary and set the relevant states. */
     @JvmOverloads
     fun show(isScrimmed: Boolean) {
-        if (primaryBouncerView.delegate == null) {
+        if (primaryBouncerView.delegate == null && !Flags.composeBouncer()) {
             Log.d(
                 TAG,
                 "PrimaryBouncerInteractor#show is being called before the " +
-                    "primaryBouncerDelegate is set. Let's exit early so we don't set the wrong " +
-                    "primaryBouncer state."
+                    "primaryBouncerDelegate is set. Let's exit early so we don't " +
+                    "set the wrong primaryBouncer state."
             )
             return
         }
@@ -272,15 +274,24 @@
         repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
     }
 
+    val bouncerDismissAction: BouncerDismissActionModel?
+        get() = repository.bouncerDismissActionModel
+
     /**
      * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
-     * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
-     * call cancelAction.
+     * unlocked, we will run the onDismissAction. If the bouncer is exited before unlocking, we call
+     * cancelAction.
      */
     fun setDismissAction(
         onDismissAction: ActivityStarter.OnDismissAction?,
         cancelAction: Runnable?
     ) {
+        repository.bouncerDismissActionModel =
+            if (onDismissAction != null && cancelAction != null) {
+                BouncerDismissActionModel(onDismissAction, cancelAction)
+            } else {
+                null
+            }
         primaryBouncerView.delegate?.setDismissAction(onDismissAction, cancelAction)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerDismissActionModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerDismissActionModel.kt
new file mode 100644
index 0000000..02b444f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/BouncerDismissActionModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.bouncer.shared.model
+
+import com.android.systemui.plugins.ActivityStarter
+
+/** Represents the action that needs to be performed after bouncer is dismissed. */
+data class BouncerDismissActionModel(
+    /** If the bouncer is unlocked, [onDismissAction] will be run. */
+    val onDismissAction: ActivityStarter.OnDismissAction?,
+    /** If the bouncer is exited before unlocking, [onCancel] will be invoked. */
+    val onCancel: Runnable?
+)
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerDialogFactory.kt
similarity index 67%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerDialogFactory.kt
index ce92b6d..5defe475 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerDialogFactory.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,10 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package com.android.systemui.bouncer.ui
+
+import android.app.AlertDialog
+
+/** Factory to create alert dialogs for use in bouncer component. */
+interface BouncerDialogFactory {
+    operator fun invoke(): AlertDialog
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
index 7f3b794..f3903de 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -16,9 +16,15 @@
 
 package com.android.systemui.bouncer.ui
 
+import android.app.AlertDialog
+import android.content.Context
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 
 @Module(
     includes =
@@ -29,4 +35,17 @@
 interface BouncerViewModule {
     /** Binds BouncerView to BouncerViewImpl and makes it injectable. */
     @Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView
+
+    companion object {
+
+        @Provides
+        @SysUISingleton
+        fun bouncerDialogFactory(@Application context: Context): BouncerDialogFactory {
+            return object : BouncerDialogFactory {
+                override fun invoke(): AlertDialog {
+                    return SystemUIDialog(context)
+                }
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
new file mode 100644
index 0000000..dd253a8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
@@ -0,0 +1,89 @@
+package com.android.systemui.bouncer.ui.binder
+
+import android.view.ViewGroup
+import com.android.keyguard.KeyguardMessageAreaController
+import com.android.keyguard.ViewMediatorCallback
+import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.systemui.Flags
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.Flags.COMPOSE_BOUNCER_ENABLED
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.log.BouncerLogger
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import dagger.Lazy
+import javax.inject.Inject
+
+/** Helper data class that allows to lazy load all the dependencies of the legacy bouncer. */
+@SysUISingleton
+data class LegacyBouncerDependencies
+@Inject
+constructor(
+    val viewModel: KeyguardBouncerViewModel,
+    val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
+    val componentFactory: KeyguardBouncerComponent.Factory,
+    val messageAreaControllerFactory: KeyguardMessageAreaController.Factory,
+    val bouncerMessageInteractor: BouncerMessageInteractor,
+    val bouncerLogger: BouncerLogger,
+    val selectedUserInteractor: SelectedUserInteractor,
+)
+
+/** Helper data class that allows to lazy load all the dependencies of the compose based bouncer. */
+@SysUISingleton
+data class ComposeBouncerDependencies
+@Inject
+constructor(
+    val legacyInteractor: PrimaryBouncerInteractor,
+    val viewModel: BouncerViewModel,
+    val dialogFactory: BouncerDialogFactory,
+    val authenticationInteractor: AuthenticationInteractor,
+    val viewMediatorCallback: ViewMediatorCallback?,
+    val selectedUserInteractor: SelectedUserInteractor,
+)
+
+/**
+ * Toggles between the compose and non compose version of the bouncer, instantiating only the
+ * dependencies required for each.
+ */
+@SysUISingleton
+class BouncerViewBinder
+@Inject
+constructor(
+    private val legacyBouncerDependencies: Lazy<LegacyBouncerDependencies>,
+    private val composeBouncerDependencies: Lazy<ComposeBouncerDependencies>,
+) {
+    fun bind(view: ViewGroup) {
+        if (
+            ComposeFacade.isComposeAvailable() && Flags.composeBouncer() && COMPOSE_BOUNCER_ENABLED
+        ) {
+            val deps = composeBouncerDependencies.get()
+            ComposeBouncerViewBinder.bind(
+                view,
+                deps.legacyInteractor,
+                deps.viewModel,
+                deps.dialogFactory,
+                deps.authenticationInteractor,
+                deps.selectedUserInteractor,
+                deps.viewMediatorCallback,
+            )
+        } else {
+            val deps = legacyBouncerDependencies.get()
+            KeyguardBouncerViewBinder.bind(
+                view,
+                deps.viewModel,
+                deps.primaryBouncerToGoneTransitionViewModel,
+                deps.componentFactory,
+                deps.messageAreaControllerFactory,
+                deps.bouncerMessageInteractor,
+                deps.bouncerLogger,
+                deps.selectedUserInteractor,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
new file mode 100644
index 0000000..7b05395
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
@@ -0,0 +1,75 @@
+package com.android.systemui.bouncer.ui.binder
+
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.ViewMediatorCallback
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+/** View binder responsible for binding the compose version of the bouncer. */
+object ComposeBouncerViewBinder {
+    fun bind(
+        view: ViewGroup,
+        legacyInteractor: PrimaryBouncerInteractor,
+        viewModel: BouncerViewModel,
+        dialogFactory: BouncerDialogFactory,
+        authenticationInteractor: AuthenticationInteractor,
+        selectedUserInteractor: SelectedUserInteractor,
+        viewMediatorCallback: ViewMediatorCallback?,
+    ) {
+        view.addView(
+            ComposeFacade.createBouncer(
+                view.context,
+                viewModel,
+                dialogFactory,
+            )
+        )
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    legacyInteractor.isShowing.collectLatest { bouncerShowing ->
+                        view.isVisible = bouncerShowing
+                    }
+                }
+
+                launch {
+                    authenticationInteractor.onAuthenticationResult.collectLatest {
+                        authenticationSucceeded ->
+                        if (authenticationSucceeded) {
+                            // Some dismiss actions require that keyguard be dismissed right away or
+                            // deferred until something else later on dismisses keyguard (eg. end of
+                            // a hide animation).
+                            val deferKeyguardDone =
+                                legacyInteractor.bouncerDismissAction?.onDismissAction?.onDismiss()
+                            legacyInteractor.setDismissAction(null, null)
+
+                            viewMediatorCallback?.let {
+                                val selectedUserId = selectedUserInteractor.getSelectedUserId()
+                                if (deferKeyguardDone == true) {
+                                    it.keyguardDonePending(selectedUserId)
+                                } else {
+                                    it.keyguardDone(selectedUserId)
+                                }
+                            }
+                        }
+                    }
+                }
+                launch {
+                    legacyInteractor.startingDisappearAnimation.collectLatest {
+                        it.run()
+                        legacyInteractor.hide()
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
index adbb37c..4184ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
@@ -31,6 +31,7 @@
         is PackageChangeModel.UpdateStarted -> "started updating"
         is PackageChangeModel.UpdateFinished -> "finished updating"
         is PackageChangeModel.Changed -> "changed"
+        is PackageChangeModel.Empty -> throw IllegalStateException("Unexpected empty value: $model")
     }
 
 /** A debug logger for [PackageChangeRepository]. */
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
index 853eff7..3ae87c3 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
@@ -23,6 +23,14 @@
     val packageName: String
     val packageUid: Int
 
+    /** Empty change, provided for convenience when a sensible default value is needed. */
+    data object Empty : PackageChangeModel {
+        override val packageName: String
+            get() = ""
+        override val packageUid: Int
+            get() = 0
+    }
+
     /**
      * An existing application package was uninstalled.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 24d4c6c..9fa4cd6 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -37,6 +37,8 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
@@ -77,6 +79,29 @@
         communalRepository.setTransitionState(transitionState)
     }
 
+    /** Returns a flow that tracks the progress of transitions to the given scene from 0-1. */
+    fun transitionProgressToScene(targetScene: CommunalSceneKey) =
+        transitionState
+            .flatMapLatest { state ->
+                when (state) {
+                    is ObservableCommunalTransitionState.Idle ->
+                        flowOf(CommunalTransitionProgress.Idle(state.scene))
+                    is ObservableCommunalTransitionState.Transition ->
+                        if (state.toScene == targetScene) {
+                            state.progress.map {
+                                CommunalTransitionProgress.Transition(
+                                    // Clamp the progress values between 0 and 1 as actual progress
+                                    // values can be higher than 0 or lower than 1 due to a fling.
+                                    progress = it.coerceIn(0.0f, 1.0f)
+                                )
+                            }
+                        } else {
+                            flowOf(CommunalTransitionProgress.OtherTransition)
+                        }
+                }
+            }
+            .distinctUntilChanged()
+
     /**
      * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the
      * [CommunalSceneKey.Communal].
@@ -232,3 +257,17 @@
         }
     }
 }
+
+/** Simplified transition progress data class for tracking a single transition between scenes. */
+sealed class CommunalTransitionProgress {
+    /** No transition/animation is currently running. */
+    data class Idle(val scene: CommunalSceneKey) : CommunalTransitionProgress()
+
+    /** There is a transition animating to the expected scene. */
+    data class Transition(
+        val progress: Float,
+    ) : CommunalTransitionProgress()
+
+    /** There is a transition animating to a scene other than the expected scene. */
+    data object OtherTransition : CommunalTransitionProgress()
+}
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 7a96fab..09c18ed 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
@@ -23,7 +23,9 @@
 import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.media.dagger.MediaModule
 import javax.inject.Inject
 import javax.inject.Named
@@ -70,6 +72,17 @@
     override val isPopupOnDismissCtaShowing: Flow<Boolean> =
         _isPopupOnDismissCtaShowing.asStateFlow()
 
+    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.
+        with(mediaHost) {
+            expansion = MediaHostState.EXPANDED
+            showsOnlyActiveMedia = false
+            falsingProtectionNeeded = false
+            init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
+        }
+    }
+
     override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor()
 
     override fun onDismissCtaTile() {
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 3a92739..acbdecc 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -22,6 +22,8 @@
 import android.view.WindowInsets
 import androidx.activity.ComponentActivity
 import androidx.lifecycle.LifecycleOwner
+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.people.ui.viewmodel.PeopleViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
@@ -88,6 +90,13 @@
         viewModel: BaseCommunalViewModel,
     ): View
 
+    /** Create a [View] to represent the [BouncerViewModel]. */
+    fun createBouncer(
+        context: Context,
+        viewModel: BouncerViewModel,
+        dialogFactory: BouncerDialogFactory,
+    ): View
+
     /** Creates a container that hosts the communal UI and handles gesture transitions. */
     fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 7876a6f..846736c 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -476,6 +476,13 @@
     // TODO(b/283300105): Tracking Bug
     @JvmField val SCENE_CONTAINER_ENABLED = false
 
+    /**
+     * Whether the compose bouncer is enabled. This ensures ProGuard can
+     * remove unused code from our APK at compile time.
+     */
+    // TODO(b/280877228): Tracking Bug
+    @JvmField val COMPOSE_BOUNCER_ENABLED = false
+
     // 1900
     @JvmField val NOTE_TASKS = releasedFlag("keycode_flag")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt
new file mode 100644
index 0000000..afe9151
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.view.View
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+@SysUISingleton
+class KeyguardSmartspaceRepository @Inject constructor() {
+    private val _bcSmartspaceVisibility: MutableStateFlow<Int> = MutableStateFlow(View.GONE)
+    val bcSmartspaceVisibility: StateFlow<Int> = _bcSmartspaceVisibility.asStateFlow()
+
+    fun setBcSmartspaceVisibility(visibility: Int) {
+        _bcSmartspaceVisibility.value = visibility
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 19fd7f9..48b3d9a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -19,6 +19,7 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 import com.android.systemui.Flags
+import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -32,6 +33,7 @@
 class FromGlanceableHubTransitionInteractor
 @Inject
 constructor(
+    private val glanceableHubTransitions: GlanceableHubTransitions,
     override val transitionRepository: KeyguardTransitionRepository,
     transitionInteractor: KeyguardTransitionInteractor,
     @Main mainDispatcher: CoroutineDispatcher,
@@ -47,6 +49,7 @@
         if (!Flags.communalHub()) {
             return
         }
+        listenForHubToLockscreen()
     }
 
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
@@ -56,6 +59,18 @@
         }
     }
 
+    /**
+     * Listens for the glanceable hub transition to lock screen and directly drives the keyguard
+     * transition.
+     */
+    private fun listenForHubToLockscreen() {
+        glanceableHubTransitions.listenForLockscreenAndHubTransition(
+            transitionName = "listenForHubToLockscreen",
+            transitionOwnerName = TAG,
+            toScene = CommunalSceneKey.Blank
+        )
+    }
+
     companion object {
         const val TAG = "FromGlanceableHubTransitionInteractor"
         val DEFAULT_DURATION = 500.milliseconds
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 2d0baa8..8b2b45f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -18,6 +18,7 @@
 
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
+import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -62,6 +63,7 @@
     private val flags: FeatureFlags,
     private val shadeRepository: ShadeRepository,
     private val powerInteractor: PowerInteractor,
+    private val glanceableHubTransitions: GlanceableHubTransitions,
     inWindowLauncherUnlockAnimationInteractor: Lazy<InWindowLauncherUnlockAnimationInteractor>,
 ) :
     TransitionInteractor(
@@ -81,6 +83,7 @@
         listenForLockscreenToPrimaryBouncerDragging()
         listenForLockscreenToAlternateBouncer()
         listenForLockscreenTransitionToCamera()
+        listenForLockscreenToGlanceableHub()
     }
 
     /**
@@ -381,6 +384,22 @@
         }
     }
 
+    /**
+     * Listens for transition from glanceable hub back to lock screen and directly drives the
+     * keyguard transition.
+     */
+    private fun listenForLockscreenToGlanceableHub() {
+        if (!com.android.systemui.Flags.communalHub()) {
+            return
+        }
+
+        glanceableHubTransitions.listenForLockscreenAndHubTransition(
+            transitionName = "listenForLockscreenToGlanceableHub",
+            transitionOwnerName = TAG,
+            toScene = CommunalSceneKey.Communal
+        )
+    }
+
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
             interpolator = Interpolators.LINEAR
@@ -406,5 +425,6 @@
         val TO_AOD_DURATION = 500.milliseconds
         val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
         val TO_GONE_DURATION = DEFAULT_DURATION
+        val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
new file mode 100644
index 0000000..cb50839
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launch
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalTransitionProgress
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+
+class GlanceableHubTransitions
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val transitionInteractor: KeyguardTransitionInteractor,
+    private val transitionRepository: KeyguardTransitionRepository,
+    private val communalInteractor: CommunalInteractor,
+) {
+    /**
+     * Listens for the glanceable hub transition to the specified scene and directly drives the
+     * keyguard transition between the lockscreen and the hub.
+     *
+     * The glanceable hub transition progress is used as the source of truth as it cannot be driven
+     * externally. The progress is used for both transitions caused by user touch input or by
+     * programmatic changes.
+     */
+    fun listenForLockscreenAndHubTransition(
+        transitionName: String,
+        transitionOwnerName: String,
+        toScene: CommunalSceneKey
+    ) {
+        val fromState: KeyguardState
+        val toState: KeyguardState
+        if (toScene == CommunalSceneKey.Blank) {
+            fromState = KeyguardState.GLANCEABLE_HUB
+            toState = KeyguardState.LOCKSCREEN
+        } else {
+            fromState = KeyguardState.LOCKSCREEN
+            toState = KeyguardState.GLANCEABLE_HUB
+        }
+        var transitionId: UUID? = null
+        scope.launch("$transitionOwnerName#$transitionName") {
+            communalInteractor
+                .transitionProgressToScene(toScene)
+                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { pair ->
+                    val (transitionProgress, lastStartedStep) = pair
+
+                    val id = transitionId
+                    if (id == null) {
+                        // No transition started.
+                        if (
+                            transitionProgress is CommunalTransitionProgress.Transition &&
+                                lastStartedStep.to == fromState
+                        ) {
+                            transitionId =
+                                transitionRepository.startTransition(
+                                    TransitionInfo(
+                                        ownerName = transitionOwnerName,
+                                        from = fromState,
+                                        to = toState,
+                                        animator = null, // transition will be manually controlled
+                                    )
+                                )
+                        }
+                    } else {
+                        if (lastStartedStep.to != toState) {
+                            return@collect
+                        }
+                        // An existing `id` means a transition is started, and calls to
+                        // `updateTransition` will control it until FINISHED or CANCELED
+                        val nextState: TransitionState
+                        val progressFraction: Float
+                        when (transitionProgress) {
+                            is CommunalTransitionProgress.Idle -> {
+                                if (transitionProgress.scene == toScene) {
+                                    nextState = TransitionState.FINISHED
+                                    progressFraction = 1f
+                                } else {
+                                    nextState = TransitionState.CANCELED
+                                    progressFraction = 0f
+                                }
+                            }
+                            is CommunalTransitionProgress.Transition -> {
+                                nextState = TransitionState.RUNNING
+                                progressFraction = transitionProgress.progress
+                            }
+                            is CommunalTransitionProgress.OtherTransition -> {
+                                // Shouldn't happen but if another transition starts during the
+                                // current one, mark the current one as canceled.
+                                nextState = TransitionState.CANCELED
+                                progressFraction = 0f
+                            }
+                        }
+                        transitionRepository.updateTransition(
+                            id,
+                            progressFraction,
+                            nextState,
+                        )
+
+                        if (
+                            nextState == TransitionState.CANCELED ||
+                                nextState == TransitionState.FINISHED
+                        ) {
+                            transitionId = null
+                        }
+
+                        // If canceled, just put the state back.
+                        if (nextState == TransitionState.CANCELED) {
+                            transitionRepository.startTransition(
+                                TransitionInfo(
+                                    ownerName = transitionOwnerName,
+                                    from = toState,
+                                    to = fromState,
+                                    animator =
+                                        ValueAnimator().apply {
+                                            interpolator = Interpolators.LINEAR
+                                            duration = 0
+                                        }
+                                )
+                            )
+                        }
+                    }
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt
new file mode 100644
index 0000000..67b5745
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardSmartspaceRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class KeyguardSmartspaceInteractor
+@Inject
+constructor(private val keyguardSmartspaceRepository: KeyguardSmartspaceRepository) {
+    var bcSmartspaceVisibility: StateFlow<Int> = keyguardSmartspaceRepository.bcSmartspaceVisibility
+
+    fun setBcSmartspaceVisibility(visibility: Int) {
+        keyguardSmartspaceRepository.setBcSmartspaceVisibility(visibility)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 5b0c562..b43ab5e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
 import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
@@ -36,17 +37,19 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.shareIn
 
 /** Encapsulates business-logic related to the keyguard transitions. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class KeyguardTransitionInteractor
 @Inject
@@ -135,10 +138,18 @@
     val lockscreenToDreamingLockscreenHostedTransition: Flow<TransitionStep> =
         repository.transition(LOCKSCREEN, DREAMING_LOCKSCREEN_HOSTED)
 
+    /** LOCKSCREEN->GLANCEABLE_HUB transition information. */
+    val lockscreenToGlanceableHubTransition: Flow<TransitionStep> =
+        repository.transition(LOCKSCREEN, GLANCEABLE_HUB)
+
     /** LOCKSCREEN->OCCLUDED transition information. */
     val lockscreenToOccludedTransition: Flow<TransitionStep> =
         repository.transition(LOCKSCREEN, OCCLUDED)
 
+    /** GLANCEABLE_HUB->LOCKSCREEN transition information. */
+    val glanceableHubToLockscreenTransition: Flow<TransitionStep> =
+        repository.transition(GLANCEABLE_HUB, LOCKSCREEN)
+
     /** OCCLUDED->LOCKSCREEN transition information. */
     val occludedToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(OCCLUDED, LOCKSCREEN)
@@ -183,29 +194,121 @@
     val finishedKeyguardTransitionStep: Flow<TransitionStep> =
         repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
 
-    /** The destination state of the last started transition. */
+    /** The destination state of the last [TransitionState.STARTED] transition. */
     val startedKeyguardState: SharedFlow<KeyguardState> =
         startedKeyguardTransitionStep
             .map { step -> step.to }
             .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
-    /** The last completed [KeyguardState] transition */
+    /**
+     * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition.
+     *
+     * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a
+     * value when a subsequent transition is STARTED. It will *only* emit once we have finally
+     * FINISHED in a state. This can have unintuitive implications.
+     *
+     * For example, if we're transitioning from GONE -> DOZING, and that transition is CANCELED in
+     * favor of a DOZING -> LOCKSCREEN transition, the FINISHED state is still GONE, and will remain
+     * GONE throughout the DOZING -> LOCKSCREEN transition until the DOZING -> LOCKSCREEN transition
+     * finishes (at which point we'll be FINISHED in LOCKSCREEN).
+     *
+     * Since there's no real limit to how many consecutive transitions can be canceled, it's even
+     * possible for the FINISHED state to be the same as the STARTED state while still
+     * transitioning.
+     *
+     * For example:
+     * 1. We're finished in GONE.
+     * 2. The user presses the power button, starting a GONE -> DOZING transition. We're still
+     *    FINISHED in GONE.
+     * 3. The user changes their mind, pressing the power button to wake up; this starts a DOZING ->
+     *    LOCKSCREEN transition. We're still FINISHED in GONE.
+     * 4. The user quickly swipes away the lockscreen prior to DOZING -> LOCKSCREEN finishing; this
+     *    starts a LOCKSCREEN -> GONE transition. We're still FINISHED in GONE, but we've also
+     *    STARTED a transition *to* GONE.
+     * 5. We'll emit KeyguardState.GONE again once the transition finishes.
+     *
+     * If you just need to know when we eventually settle into a state, this flow is likely
+     * sufficient. However, if you're having issues with state *during* transitions started after
+     * one or more canceled transitions, you probably need to use [currentKeyguardState].
+     */
     val finishedKeyguardState: SharedFlow<KeyguardState> =
         finishedKeyguardTransitionStep
             .map { step -> step.to }
             .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
     /**
-     * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed
-     * it.
+     * The [KeyguardState] we're currently in.
+     *
+     * If we're not in transition, this is simply the [finishedKeyguardState]. If we're in
+     * transition, this is the state we're transitioning *from*.
+     *
+     * Absent CANCELED transitions, [currentKeyguardState] and [finishedKeyguardState] are always
+     * identical - if a transition FINISHES in a given state, the subsequent state we START a
+     * transition *from* would always be that same previously FINISHED state.
+     *
+     * However, if a transition is CANCELED, the next transition will START from a state we never
+     * FINISHED in. For example, if we transition from GONE -> DOZING, but CANCEL that transition in
+     * favor of DOZING -> LOCKSCREEN, we've STARTED a transition *from* DOZING despite never
+     * FINISHING in DOZING. Thus, the current state will be DOZING but the FINISHED state will still
+     * be GONE.
+     *
+     * In this example, if there was DOZING-related state that needs to be set up in order to
+     * properly render a DOZING -> LOCKSCREEN transition, it would never be set up if we were
+     * listening for [finishedKeyguardState] to emit DOZING. However, [currentKeyguardState] would
+     * emit DOZING immediately upon STARTING DOZING -> LOCKSCREEN, allowing us to set up the state.
+     *
+     * Whether you want to use [currentKeyguardState] or [finishedKeyguardState] depends on your
+     * specific use case and how you want to handle cancellations. In general, if you're dealing
+     * with state/UI present across multiple [KeyguardState]s, you probably want
+     * [currentKeyguardState]. If you're dealing with state/UI encapsulated within a single state,
+     * you likely want [finishedKeyguardState].
+     *
+     * As an example, let's say you want to animate in a message on the lockscreen UI after waking
+     * up, and that TextView is not involved in animations between states. You'd want to collect
+     * [finishedKeyguardState], so you'll only animate it in once we're settled on the lockscreen.
+     * If you use [currentKeyguardState] in this case, a DOZING -> LOCKSCREEN transition that is
+     * interrupted by a LOCKSCREEN -> GONE transition would cause the message to become visible
+     * immediately upon LOCKSCREEN -> GONE STARTING, as the current state would become LOCKSCREEN in
+     * that case. That's likely not what you want.
+     *
+     * On the other hand, let's say you're animating the smartspace from alpha 0f to 1f during
+     * DOZING -> LOCKSCREEN, but the transition is interrupted by LOCKSCREEN -> GONE. LS -> GONE
+     * needs the smartspace to be alpha=1f so that it can play the shared-element unlock animation.
+     * In this case, we'd want to collect [currentKeyguardState] and ensure the smartspace is
+     * visible when the current state is LOCKSCREEN. If you use [finishedKeyguardState] in this
+     * case, the smartspace will never be set to alpha = 1f and you'll have a half-faded smartspace
+     * during the LS -> GONE transition.
+     *
+     * If you need special-case handling for cancellations (such as conditional handling depending
+     * on which [KeyguardState] was canceled) you can collect [canceledKeyguardTransitionStep]
+     * directly.
+     *
+     * As a helpful footnote, here's the values of [finishedKeyguardState] and
+     * [currentKeyguardState] during a sequence with two cancellations:
+     * 1. We're FINISHED in GONE. currentKeyguardState=GONE; finishedKeyguardState=GONE.
+     * 2. We START a transition from GONE -> DOZING. currentKeyguardState=GONE;
+     *    finishedKeyguardState=GONE.
+     * 3. We CANCEL this transition and START a transition from DOZING -> LOCKSCREEN.
+     *    currentKeyguardState=DOZING; finishedKeyguardState=GONE.
+     * 4. We subsequently also CANCEL DOZING -> LOCKSCREEN and START LOCKSCREEN -> GONE.
+     *    currentKeyguardState=LOCKSCREEN finishedKeyguardState=GONE.
+     * 5. LOCKSCREEN -> GONE is allowed to FINISH. currentKeyguardState=GONE;
+     *    finishedKeyguardState=GONE.
      */
-    val isInTransitionToAnyState =
-        combine(
-            startedKeyguardTransitionStep,
-            finishedKeyguardState,
-        ) { startedStep, finishedState ->
-            startedStep.to != finishedState
-        }
+    val currentKeyguardState: SharedFlow<KeyguardState> =
+        repository.transitions
+            .mapLatest {
+                if (it.transitionState == TransitionState.FINISHED) {
+                    it.to
+                } else {
+                    it.from
+                }
+            }
+            .distinctUntilChanged()
+            .shareIn(scope, SharingStarted.Eagerly, replay = 1)
+
+    /** Whether we've currently STARTED a transition and haven't yet FINISHED it. */
+    val isInTransitionToAnyState = isInTransitionWhere({ true }, { true })
 
     /**
      * The amount of transition into or out of the given [KeyguardState].
@@ -295,13 +398,12 @@
         fromStatePredicate: (KeyguardState) -> Boolean,
         toStatePredicate: (KeyguardState) -> Boolean,
     ): Flow<Boolean> {
-        return combine(
-                startedKeyguardTransitionStep,
-                finishedKeyguardState,
-            ) { startedStep, finishedState ->
-                fromStatePredicate(startedStep.from) &&
-                    toStatePredicate(startedStep.to) &&
-                    finishedState != startedStep.to
+        return repository.transitions
+            .filter { it.transitionState != TransitionState.CANCELED }
+            .mapLatest {
+                it.transitionState != TransitionState.FINISHED &&
+                    fromStatePredicate(it.from) &&
+                    toStatePredicate(it.to)
             }
             .distinctUntilChanged()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index bf763b4..400b8bf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -30,7 +30,10 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
+import com.android.keyguard.KeyguardClockSwitch.LARGE
+import com.android.keyguard.KeyguardClockSwitch.SMALL
 import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
@@ -48,6 +51,7 @@
         keyguardRootView: ConstraintLayout,
         viewModel: KeyguardClockViewModel,
         keyguardClockInteractor: KeyguardClockInteractor,
+        blueprintInteractor: KeyguardBlueprintInteractor,
     ) {
         keyguardRootView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
@@ -61,18 +65,16 @@
                     viewModel.currentClock.collect { currentClock ->
                         cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer)
                         viewModel.clock = currentClock
-                        addClockViews(currentClock, keyguardRootView, viewModel.burnInLayer)
-                        viewModel.burnInLayer?.updatePostLayout(keyguardRootView)
-                        applyConstraints(clockSection, keyguardRootView, true)
+                        addClockViews(currentClock, keyguardRootView)
+                        updateBurnInLayer(keyguardRootView, viewModel)
+                        blueprintInteractor.refreshBlueprint()
                     }
                 }
-                // TODO: Weather clock dozing animation
-                // will trigger both shouldBeCentered and clockSize change
-                // we should avoid this
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
                     viewModel.clockSize.collect {
-                        applyConstraints(clockSection, keyguardRootView, true)
+                        updateBurnInLayer(keyguardRootView, viewModel)
+                        blueprintInteractor.refreshBlueprint()
                     }
                 }
                 launch {
@@ -82,7 +84,7 @@
                             if (it.largeClock.config.hasCustomPositionUpdatedAnimation) {
                                 playClockCenteringAnimation(clockSection, keyguardRootView, it)
                             } else {
-                                applyConstraints(clockSection, keyguardRootView, true)
+                                blueprintInteractor.refreshBlueprint()
                             }
                         }
                     }
@@ -90,6 +92,29 @@
             }
         }
     }
+    @VisibleForTesting
+    fun updateBurnInLayer(
+        keyguardRootView: ConstraintLayout,
+        viewModel: KeyguardClockViewModel,
+    ) {
+        val burnInLayer = viewModel.burnInLayer
+        val clockController = viewModel.currentClock.value
+        clockController?.let { clock ->
+            when (viewModel.clockSize.value) {
+                LARGE -> {
+                    clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) }
+                    if (clock.config.useAlternateSmartspaceAODTransition) {
+                        clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) }
+                    }
+                }
+                SMALL -> {
+                    clock.smallClock.layout.views.forEach { burnInLayer?.addView(it) }
+                    clock.largeClock.layout.views.forEach { burnInLayer?.removeView(it) }
+                }
+            }
+        }
+        viewModel.burnInLayer?.updatePostLayout(keyguardRootView)
+    }
 
     private fun cleanupClockViews(
         clockController: ClockController?,
@@ -116,7 +141,6 @@
     fun addClockViews(
         clockController: ClockController?,
         rootView: ConstraintLayout,
-        burnInLayer: Layer?
     ) {
         clockController?.let { clock ->
             clock.smallClock.layout.views[0].id = R.id.lockscreen_clock_view
@@ -125,17 +149,10 @@
             }
             // small clock should either be a single view or container with id
             // `lockscreen_clock_view`
-            clock.smallClock.layout.views.forEach {
-                rootView.addView(it)
-                burnInLayer?.addView(it)
-            }
+            clock.smallClock.layout.views.forEach { rootView.addView(it) }
             clock.largeClock.layout.views.forEach { rootView.addView(it) }
-            if (clock.config.useAlternateSmartspaceAODTransition) {
-                clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) }
-            }
         }
     }
-
     fun applyConstraints(
         clockSection: ClockSection,
         rootView: ConstraintLayout,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index 81ce8f0..10392e3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -16,15 +16,13 @@
 
 package com.android.systemui.keyguard.ui.binder
 
-import android.transition.TransitionManager
 import android.view.View
 import androidx.constraintlayout.helper.widget.Layer
 import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -35,10 +33,10 @@
 object KeyguardSmartspaceViewBinder {
     @JvmStatic
     fun bind(
-        smartspaceSection: SmartspaceSection,
         keyguardRootView: ConstraintLayout,
         clockViewModel: KeyguardClockViewModel,
         smartspaceViewModel: KeyguardSmartspaceViewModel,
+        blueprintInteractor: KeyguardBlueprintInteractor,
     ) {
         keyguardRootView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -46,22 +44,56 @@
                     if (!migrateClocksToBlueprint()) return@launch
                     clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
                         ->
-                        if (hasCustomWeatherDataDisplay) {
-                            removeDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel)
-                        } else {
-                            addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel)
-                        }
-                        clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView)
-                        val constraintSet = ConstraintSet().apply { clone(keyguardRootView) }
-                        smartspaceSection.applyConstraints(constraintSet)
-                        TransitionManager.beginDelayedTransition(keyguardRootView)
-                        constraintSet.applyTo(keyguardRootView)
+                        updateDateWeatherToBurnInLayer(
+                            keyguardRootView,
+                            clockViewModel,
+                            smartspaceViewModel
+                        )
+                        blueprintInteractor.refreshBlueprint()
+                    }
+                }
+
+                launch {
+                    smartspaceViewModel.bcSmartspaceVisibility.collect {
+                        updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel)
+                        blueprintInteractor.refreshBlueprint()
                     }
                 }
             }
         }
     }
 
+    private fun updateBCSmartspaceInBurnInLayer(
+        keyguardRootView: ConstraintLayout,
+        clockViewModel: KeyguardClockViewModel,
+    ) {
+        // Visibility is controlled by updateTargetVisibility in CardPagerAdapter
+        val burnInLayer = keyguardRootView.requireViewById<Layer>(R.id.burn_in_layer)
+        burnInLayer.apply {
+            val smartspaceView =
+                keyguardRootView.requireViewById<View>(sharedR.id.bc_smartspace_view)
+            if (smartspaceView.visibility == View.VISIBLE) {
+                addView(smartspaceView)
+            } else {
+                removeView(smartspaceView)
+            }
+        }
+        clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView)
+    }
+
+    private fun updateDateWeatherToBurnInLayer(
+        keyguardRootView: ConstraintLayout,
+        clockViewModel: KeyguardClockViewModel,
+        smartspaceViewModel: KeyguardSmartspaceViewModel
+    ) {
+        if (clockViewModel.hasCustomWeatherDataDisplay.value) {
+            removeDateWeatherFromBurnInLayer(keyguardRootView, smartspaceViewModel)
+        } else {
+            addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel)
+        }
+        clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView)
+    }
+
     private fun addDateWeatherToBurnInLayer(
         constraintLayout: ConstraintLayout,
         smartspaceViewModel: KeyguardSmartspaceViewModel
@@ -76,13 +108,13 @@
                     constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view)
                 val weatherView =
                     constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view)
-                addView(weatherView)
                 addView(dateView)
+                addView(weatherView)
             }
         }
     }
 
-    private fun removeDateWeatherToBurnInLayer(
+    private fun removeDateWeatherFromBurnInLayer(
         constraintLayout: ConstraintLayout,
         smartspaceViewModel: KeyguardSmartspaceViewModel
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
index 24d0602..8472a9f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
+import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
@@ -30,11 +31,10 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
 import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
-import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeClockSection
+import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeMediaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeNotificationStackScrollLayoutSection
-import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeSmartspaceSection
 import com.android.systemui.util.kotlin.getOrNull
 import java.util.Optional
 import javax.inject.Inject
@@ -62,8 +62,8 @@
     aodNotificationIconsSection: AodNotificationIconsSection,
     aodBurnInSection: AodBurnInSection,
     communalTutorialIndicatorSection: CommunalTutorialIndicatorSection,
-    smartspaceSection: SplitShadeSmartspaceSection,
-    clockSection: SplitShadeClockSection,
+    clockSection: ClockSection,
+    smartspaceSection: SmartspaceSection,
     mediaSection: SplitShadeMediaSection,
 ) : KeyguardBlueprint {
     override val id: String = ID
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
index d0626d5..fd530b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
@@ -25,6 +25,8 @@
 import android.view.View
 import android.view.ViewGroup
 import androidx.constraintlayout.helper.widget.Layer
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
+import com.android.systemui.res.R
 
 class BaseBlueprintTransition : TransitionSet() {
     init {
@@ -33,7 +35,16 @@
             .addTransition(ChangeBounds())
             .addTransition(AlphaInVisibility())
         excludeTarget(Layer::class.java, /* exclude= */ true)
+        excludeClockAndSmartspaceViews()
     }
+
+    private fun excludeClockAndSmartspaceViews() {
+        excludeTarget(R.id.lockscreen_clock_view, true)
+        excludeTarget(R.id.lockscreen_clock_view_large, true)
+        excludeTarget(SmartspaceView::class.java, true)
+        // TODO(b/319468190): need to exclude views from large weather clock
+    }
+
     class AlphaOutVisibility : Visibility() {
         override fun onDisappear(
             sceneRoot: ViewGroup?,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt
new file mode 100644
index 0000000..67a20e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.view.layout.sections
+
+import android.content.Context
+import android.view.View
+import androidx.constraintlayout.helper.widget.Layer
+
+class AodBurnInLayer(context: Context) : Layer(context) {
+    // For setScale in Layer class, it stores it in mScaleX/Y and directly apply scale to
+    // referenceViews instead of keeping the value in fields of View class
+    // when we try to clone ConstraintSet, it will call getScaleX from View class and return 1.0
+    // and when we clone and apply, it will reset everything in the layer
+    // which cause the flicker from AOD to LS
+    private var _scaleX = 1F
+    private var _scaleY = 1F
+    // As described for _scaleX and _scaleY, we have similar issue with translation
+    private var _translationX = 1F
+    private var _translationY = 1F
+    // avoid adding views with same ids
+    override fun addView(view: View?) {
+        view?.let { if (it.id !in referencedIds) super.addView(view) }
+    }
+    override fun setScaleX(scaleX: Float) {
+        _scaleX = scaleX
+        super.setScaleX(scaleX)
+    }
+
+    override fun getScaleX(): Float {
+        return _scaleX
+    }
+
+    override fun setScaleY(scaleY: Float) {
+        _scaleY = scaleY
+        super.setScaleY(scaleY)
+    }
+
+    override fun getScaleY(): Float {
+        return _scaleY
+    }
+
+    override fun setTranslationX(dx: Float) {
+        _translationX = dx
+        super.setTranslationX(dx)
+    }
+
+    override fun getTranslationX(): Float {
+        return _translationX
+    }
+
+    override fun setTranslationY(dy: Float) {
+        _translationY = dy
+        super.setTranslationY(dy)
+    }
+
+    override fun getTranslationY(): Float {
+        return _translationY
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
index 1ccc6cc..3d36eb0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
@@ -19,16 +19,13 @@
 
 import android.content.Context
 import android.view.View
-import androidx.constraintlayout.helper.widget.Layer
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.res.R
-import com.android.systemui.shared.R as sharedR
 import javax.inject.Inject
 
 /** Adds a layer to group elements for translation for burn-in preventation */
@@ -37,10 +34,8 @@
 constructor(
     private val context: Context,
     private val clockViewModel: KeyguardClockViewModel,
-    private val smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) : KeyguardSection() {
-    lateinit var burnInLayer: Layer
-
+    private lateinit var burnInLayer: AodBurnInLayer
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!KeyguardShadeMigrationNssl.isEnabled) {
             return
@@ -48,7 +43,7 @@
 
         val nic = constraintLayout.requireViewById<View>(R.id.aod_notification_icon_container)
         burnInLayer =
-            Layer(context).apply {
+            AodBurnInLayer(context).apply {
                 id = R.id.burn_in_layer
                 addView(nic)
                 if (!migrateClocksToBlueprint()) {
@@ -57,11 +52,6 @@
                     addView(statusView)
                 }
             }
-        if (migrateClocksToBlueprint()) {
-            // weather and date parts won't be added here, cause their visibility doesn't align
-            // with others in burnInLayer
-            addSmartspaceViews(constraintLayout)
-        }
         constraintLayout.addView(burnInLayer)
     }
 
@@ -83,14 +73,4 @@
     override fun removeViews(constraintLayout: ConstraintLayout) {
         constraintLayout.removeView(R.id.burn_in_layer)
     }
-
-    private fun addSmartspaceViews(constraintLayout: ConstraintLayout) {
-        burnInLayer.apply {
-            if (smartspaceViewModel.isSmartspaceEnabled) {
-                val smartspaceView =
-                    constraintLayout.requireViewById<View>(sharedR.id.bc_smartspace_view)
-                addView(smartspaceView)
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index f560b5f..ed7abff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -30,9 +30,7 @@
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.shared.model.KeyguardSection
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.res.R
-import com.android.systemui.shared.R as sharedR
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
@@ -53,13 +51,13 @@
     private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
     private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
     private val notificationIconAreaController: NotificationIconAreaController,
-    private val smartspaceViewModel: KeyguardSmartspaceViewModel,
     private val systemBarUtilsState: SystemBarUtilsState,
 ) : KeyguardSection() {
 
     private var nicBindingDisposable: DisposableHandle? = null
     private val nicId = R.id.aod_notification_icon_container
     private lateinit var nic: NotificationIconContainer
+    private val smartSpaceBarrier = View.generateViewId()
 
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!KeyguardShadeMigrationNssl.isEnabled) {
@@ -118,7 +116,7 @@
             }
         constraintSet.apply {
             if (migrateClocksToBlueprint()) {
-                connect(nicId, TOP, sharedR.id.bc_smartspace_view, BOTTOM, bottomMargin)
+                connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin)
                 setGoneMargin(nicId, BOTTOM, bottomMargin)
             } else {
                 connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index b5f32c8..b344d3b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -23,11 +23,14 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
 import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.INVISIBLE
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.constraintlayout.widget.ConstraintSet.VISIBLE
 import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import com.android.systemui.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder
@@ -35,8 +38,10 @@
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockFaceLayout
 import com.android.systemui.res.R
+import com.android.systemui.shared.R as sharedR
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import com.android.systemui.util.Utils
+import dagger.Lazy
 import javax.inject.Inject
 
 internal fun ConstraintSet.setVisibility(
@@ -56,6 +61,7 @@
     protected val keyguardClockViewModel: KeyguardClockViewModel,
     private val context: Context,
     private val splitShadeStateController: SplitShadeStateController,
+    val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
 ) : KeyguardSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {}
 
@@ -68,6 +74,7 @@
             constraintLayout,
             keyguardClockViewModel,
             clockInteractor,
+            blueprintInteractor.get()
         )
     }
 
@@ -88,12 +95,16 @@
     ): ConstraintSet {
         // Add constraint between rootView and clockContainer
         applyDefaultConstraints(constraintSet)
+        getNonTargetClockFace(clock).applyConstraints(constraintSet)
         getTargetClockFace(clock).applyConstraints(constraintSet)
 
         // Add constraint between elements in clock and clock container
         return constraintSet.apply {
-            setAlpha(getTargetClockFace(clock).views, 1F)
-            setAlpha(getNonTargetClockFace(clock).views, 0F)
+            setVisibility(getTargetClockFace(clock).views, VISIBLE)
+            setVisibility(getNonTargetClockFace(clock).views, INVISIBLE)
+            if (!keyguardClockViewModel.useLargeClock) {
+                connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM)
+            }
         }
     }
 
@@ -107,9 +118,12 @@
     private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout
     private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout
     open fun applyDefaultConstraints(constraints: ConstraintSet) {
+        val guideline =
+            if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID
+            else R.id.split_shade_guideline
         constraints.apply {
             connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START)
-            connect(R.id.lockscreen_clock_view_large, END, PARENT_ID, END)
+            connect(R.id.lockscreen_clock_view_large, END, guideline, END)
             connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP)
             var largeClockTopMargin =
                 context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
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 b0eee0a..8c5e9b4 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
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.ui.view.layout.sections
 
 import android.content.Context
+import android.view.View
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
 import androidx.constraintlayout.widget.ConstraintSet.END
@@ -27,11 +28,9 @@
 import com.android.systemui.Flags.migrateClocksToBlueprint
 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.NotificationPanelView
-import com.android.systemui.shared.R as sharedR
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
@@ -54,7 +53,6 @@
     ambientState: AmbientState,
     controller: NotificationStackScrollLayoutController,
     notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    private val smartspaceViewModel: KeyguardSmartspaceViewModel,
     @Main mainDispatcher: CoroutineDispatcher,
 ) :
     NotificationStackScrollLayoutSection(
@@ -69,6 +67,7 @@
         notificationStackSizeCalculator,
         mainDispatcher,
     ) {
+    private val smartSpaceBarrier = View.generateViewId()
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!KeyguardShadeMigrationNssl.isEnabled) {
             return
@@ -76,16 +75,14 @@
         constraintSet.apply {
             val bottomMargin =
                 context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
-
             if (migrateClocksToBlueprint()) {
                 connect(
                     R.id.nssl_placeholder,
                     TOP,
-                    sharedR.id.bc_smartspace_view,
+                    R.id.smart_space_barrier_bottom,
                     BOTTOM,
                     bottomMargin
                 )
-                setGoneMargin(R.id.nssl_placeholder, TOP, bottomMargin)
             } 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/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index eacd466..37842a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -18,37 +18,41 @@
 
 import android.content.Context
 import android.view.View
+import android.view.View.GONE
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import androidx.constraintlayout.widget.Barrier
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.END
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.START
-import androidx.constraintlayout.widget.ConstraintSet.TOP
-import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardSmartspaceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
-import com.android.systemui.res.R
+import com.android.systemui.shared.R
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
+import dagger.Lazy
 import javax.inject.Inject
 
 open class SmartspaceSection
 @Inject
 constructor(
+    val context: Context,
     val keyguardClockViewModel: KeyguardClockViewModel,
     val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel,
-    private val context: Context,
+    val keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor,
     val smartspaceController: LockscreenSmartspaceController,
     val keyguardUnlockAnimationController: KeyguardUnlockAnimationController,
+    val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
 ) : KeyguardSection() {
     private var smartspaceView: View? = null
     private var weatherView: View? = null
     private var dateView: View? = null
 
+    private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null
+
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!migrateClocksToBlueprint()) {
             return
@@ -64,6 +68,20 @@
             }
         }
         keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView
+        smartspaceVisibilityListener =
+            object : OnGlobalLayoutListener {
+                var pastVisibility = GONE
+                override fun onGlobalLayout() {
+                    smartspaceView?.let {
+                        val newVisibility = it.visibility
+                        if (pastVisibility != newVisibility) {
+                            keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility)
+                            pastVisibility = newVisibility
+                        }
+                    }
+                }
+            }
+        smartspaceView?.viewTreeObserver?.addOnGlobalLayoutListener(smartspaceVisibilityListener)
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
@@ -71,10 +89,10 @@
             return
         }
         KeyguardSmartspaceViewBinder.bind(
-            this,
             constraintLayout,
             keyguardClockViewModel,
             keyguardSmartspaceViewModel,
+            blueprintInteractor.get(),
         )
     }
 
@@ -82,65 +100,96 @@
         if (!migrateClocksToBlueprint()) {
             return
         }
-        // Generally, weather should be next to dateView
-        // smartspace should be below date & weather views
         constraintSet.apply {
             // migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController
-            dateView?.let { dateView ->
-                constrainHeight(dateView.id, WRAP_CONTENT)
-                constrainWidth(dateView.id, WRAP_CONTENT)
-                connect(
-                    dateView.id,
-                    START,
-                    PARENT_ID,
-                    START,
-                    context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)
+            constrainHeight(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
+            constrainWidth(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
+            connect(
+                R.id.date_smartspace_view,
+                ConstraintSet.START,
+                ConstraintSet.PARENT_ID,
+                ConstraintSet.START,
+                context.resources.getDimensionPixelSize(
+                    com.android.systemui.res.R.dimen.below_clock_padding_start
                 )
-            }
-            weatherView?.let {
-                constrainWidth(it.id, WRAP_CONTENT)
-                dateView?.let { dateView ->
-                    connect(it.id, TOP, dateView.id, TOP)
-                    connect(it.id, BOTTOM, dateView.id, BOTTOM)
-                    connect(it.id, START, dateView.id, END, 4)
-                }
-            }
+            )
+            constrainWidth(R.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT)
+            connect(
+                R.id.weather_smartspace_view,
+                ConstraintSet.TOP,
+                R.id.date_smartspace_view,
+                ConstraintSet.TOP
+            )
+            connect(
+                R.id.weather_smartspace_view,
+                ConstraintSet.BOTTOM,
+                R.id.date_smartspace_view,
+                ConstraintSet.BOTTOM
+            )
+            connect(
+                R.id.weather_smartspace_view,
+                ConstraintSet.START,
+                R.id.date_smartspace_view,
+                ConstraintSet.END,
+                4
+            )
+
             // migrate addSmartspaceView from KeyguardClockSwitchController
-            smartspaceView?.let {
-                constrainHeight(it.id, WRAP_CONTENT)
+            constrainHeight(R.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT)
+            connect(
+                R.id.bc_smartspace_view,
+                ConstraintSet.START,
+                ConstraintSet.PARENT_ID,
+                ConstraintSet.START,
+                context.resources.getDimensionPixelSize(
+                    com.android.systemui.res.R.dimen.below_clock_padding_start
+                )
+            )
+            connect(
+                R.id.bc_smartspace_view,
+                ConstraintSet.END,
+                if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID
+                else com.android.systemui.res.R.id.split_shade_guideline,
+                ConstraintSet.END,
+                context.resources.getDimensionPixelSize(
+                    com.android.systemui.res.R.dimen.below_clock_padding_end
+                )
+            )
+
+            if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
+                clear(R.id.date_smartspace_view, ConstraintSet.TOP)
                 connect(
-                    it.id,
-                    START,
-                    PARENT_ID,
-                    START,
-                    context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)
+                    R.id.date_smartspace_view,
+                    ConstraintSet.BOTTOM,
+                    R.id.bc_smartspace_view,
+                    ConstraintSet.TOP
+                )
+            } else {
+                clear(R.id.date_smartspace_view, ConstraintSet.BOTTOM)
+                connect(
+                    R.id.date_smartspace_view,
+                    ConstraintSet.TOP,
+                    com.android.systemui.res.R.id.lockscreen_clock_view,
+                    ConstraintSet.BOTTOM
                 )
                 connect(
-                    it.id,
-                    END,
-                    PARENT_ID,
-                    END,
-                    context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end)
+                    R.id.bc_smartspace_view,
+                    ConstraintSet.TOP,
+                    R.id.date_smartspace_view,
+                    ConstraintSet.BOTTOM
                 )
             }
 
-            if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
-                dateView?.let { dateView ->
-                    smartspaceView?.let { smartspaceView ->
-                        connect(dateView.id, BOTTOM, smartspaceView.id, TOP)
-                    }
-                }
-            } else {
-                dateView?.let { dateView ->
-                    clear(dateView.id, BOTTOM)
-                    connect(dateView.id, TOP, R.id.lockscreen_clock_view, BOTTOM)
-                    constrainHeight(dateView.id, WRAP_CONTENT)
-                    smartspaceView?.let { smartspaceView ->
-                        clear(smartspaceView.id, TOP)
-                        connect(smartspaceView.id, TOP, dateView.id, BOTTOM)
-                    }
-                }
-            }
+            createBarrier(
+                com.android.systemui.res.R.id.smart_space_barrier_bottom,
+                Barrier.BOTTOM,
+                0,
+                *intArrayOf(
+                    R.id.bc_smartspace_view,
+                    R.id.date_smartspace_view,
+                    R.id.weather_smartspace_view,
+                )
+            )
         }
         updateVisibility(constraintSet)
     }
@@ -156,30 +205,28 @@
                 }
             }
         }
+        smartspaceView?.viewTreeObserver?.removeOnGlobalLayoutListener(smartspaceVisibilityListener)
+        smartspaceVisibilityListener = null
     }
 
     private fun updateVisibility(constraintSet: ConstraintSet) {
         constraintSet.apply {
-            weatherView?.let {
-                setVisibility(
-                    it.id,
-                    when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
-                        true -> ConstraintSet.GONE
-                        false ->
-                            when (keyguardSmartspaceViewModel.isWeatherEnabled) {
-                                true -> ConstraintSet.VISIBLE
-                                false -> ConstraintSet.GONE
-                            }
-                    }
-                )
-            }
-            dateView?.let {
-                setVisibility(
-                    it.id,
-                    if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE
-                    else ConstraintSet.VISIBLE
-                )
-            }
+            setVisibility(
+                R.id.weather_smartspace_view,
+                when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
+                    true -> ConstraintSet.GONE
+                    false ->
+                        when (keyguardSmartspaceViewModel.isWeatherEnabled) {
+                            true -> ConstraintSet.VISIBLE
+                            false -> ConstraintSet.GONE
+                        }
+                }
+            )
+            setVisibility(
+                R.id.date_smartspace_view,
+                if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE
+                else ConstraintSet.VISIBLE
+            )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt
deleted file mode 100644
index 19ba1aa..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.view.layout.sections
-
-import android.content.Context
-import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.policy.SplitShadeStateController
-import javax.inject.Inject
-
-class SplitShadeClockSection
-@Inject
-constructor(
-    clockInteractor: KeyguardClockInteractor,
-    keyguardClockViewModel: KeyguardClockViewModel,
-    context: Context,
-    splitShadeStateController: SplitShadeStateController,
-) : ClockSection(clockInteractor, keyguardClockViewModel, context, splitShadeStateController) {
-    override fun applyDefaultConstraints(constraints: ConstraintSet) {
-        super.applyDefaultConstraints(constraints)
-        val largeClockEndGuideline =
-            if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID
-            else R.id.split_shade_guideline
-        constraints.apply {
-            connect(
-                R.id.lockscreen_clock_view_large,
-                ConstraintSet.END,
-                largeClockEndGuideline,
-                ConstraintSet.END
-            )
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
index f20ab06..b12a8a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
@@ -20,7 +20,6 @@
 import android.view.View
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
-import androidx.constraintlayout.widget.Barrier
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
@@ -31,11 +30,9 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.media.controls.ui.KeyguardMediaController
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.shared.R as sharedR
 import javax.inject.Inject
 
 /** Aligns media on left side for split shade, below smartspace, date, and weather. */
@@ -44,11 +41,9 @@
 constructor(
     private val context: Context,
     private val notificationPanelView: NotificationPanelView,
-    private val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel,
     private val keyguardMediaController: KeyguardMediaController
 ) : KeyguardSection() {
     private val mediaContainerId = R.id.status_view_media_container
-    private val smartSpaceBarrier = R.id.smart_space_barrier_bottom
 
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!migrateClocksToBlueprint()) {
@@ -85,18 +80,7 @@
         constraintSet.apply {
             constrainWidth(mediaContainerId, MATCH_CONSTRAINT)
             constrainHeight(mediaContainerId, WRAP_CONTENT)
-
-            createBarrier(
-                smartSpaceBarrier,
-                Barrier.BOTTOM,
-                0,
-                *intArrayOf(
-                    sharedR.id.bc_smartspace_view,
-                    sharedR.id.date_smartspace_view,
-                    sharedR.id.weather_smartspace_view,
-                )
-            )
-            connect(mediaContainerId, TOP, smartSpaceBarrier, BOTTOM)
+            connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM)
             connect(mediaContainerId, START, PARENT_ID, START)
             connect(mediaContainerId, END, R.id.split_shade_guideline, END)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt
deleted file mode 100644
index 8728ada..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.view.layout.sections
-
-import android.content.Context
-import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
-import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
-import javax.inject.Inject
-
-/*
- * We need this class for the splitShadeBlueprint so `addViews` and `removeViews` will be called
- * when switching to and from splitShade.
- */
-class SplitShadeSmartspaceSection
-@Inject
-constructor(
-    keyguardClockViewModel: KeyguardClockViewModel,
-    keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel,
-    context: Context,
-    smartspaceController: LockscreenSmartspaceController,
-    keyguardUnlockAnimationController: KeyguardUnlockAnimationController,
-) :
-    SmartspaceSection(
-        keyguardClockViewModel,
-        keyguardSmartspaceViewModel,
-        context,
-        smartspaceController,
-        keyguardUnlockAnimationController,
-    )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
index fa4de04..ce45112 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
@@ -17,14 +17,12 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.Context
-import android.hardware.biometrics.SensorLocationInternal
 import com.android.settingslib.Utils
-import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
+import com.android.systemui.biometrics.domain.interactor.FingerprintPropertyInteractor
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
-import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -44,21 +42,24 @@
     configurationInteractor: ConfigurationInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     deviceEntryBackgroundViewModel: DeviceEntryBackgroundViewModel,
-    fingerprintPropertyRepository: FingerprintPropertyRepository,
+    fingerprintPropertyInteractor: FingerprintPropertyInteractor,
     udfpsOverlayInteractor: UdfpsOverlayInteractor,
 ) {
     private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
+
+    /**
+     * UDFPS icon location in pixels for the current display and screen resolution, in natural
+     * orientation.
+     */
     val iconLocation: Flow<IconLocation> =
         isSupported.flatMapLatest { supportsUI ->
             if (supportsUI) {
-                fingerprintPropertyRepository.sensorLocations.map { sensorLocations ->
-                    val sensorLocation =
-                        sensorLocations.getOrDefault("", SensorLocationInternal.DEFAULT)
+                fingerprintPropertyInteractor.sensorLocation.map { sensorLocation ->
                     IconLocation(
-                        left = sensorLocation.sensorLocationX - sensorLocation.sensorRadius,
-                        top = sensorLocation.sensorLocationY - sensorLocation.sensorRadius,
-                        right = sensorLocation.sensorLocationX + sensorLocation.sensorRadius,
-                        bottom = sensorLocation.sensorLocationY + sensorLocation.sensorRadius,
+                        left = (sensorLocation.centerX - sensorLocation.radius).toInt(),
+                        top = (sensorLocation.centerY - sensorLocation.radius).toInt(),
+                        right = (sensorLocation.centerX + sensorLocation.radius).toInt(),
+                        bottom = (sensorLocation.centerY + sensorLocation.radius).toInt(),
                     )
                 }
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..bc51821
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+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 javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down GLANCEABLE_HUB->LOCKSCREEN transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class GlanceableHubToLockscreenTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            from = KeyguardState.GLANCEABLE_HUB,
+            to = KeyguardState.LOCKSCREEN,
+        )
+
+    // TODO(b/315205222): implement full animation spec instead of just a simple fade.
+    val keyguardAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            onStep = { it },
+            onFinish = { 1f },
+            onCancel = { 0f },
+            name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha",
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 5bb2782..f37d9f8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -99,34 +99,4 @@
             context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
                 Utils.getStatusBarHeaderHeightKeyguard(context)
         }
-
-    fun getLargeClockTopMargin(context: Context): Int {
-        var largeClockTopMargin =
-            context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
-                context.resources.getDimensionPixelSize(
-                    com.android.systemui.customization.R.dimen.small_clock_padding_top
-                ) +
-                context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
-        largeClockTopMargin += getDimen(context, DATE_WEATHER_VIEW_HEIGHT)
-        largeClockTopMargin += getDimen(context, ENHANCED_SMARTSPACE_HEIGHT)
-        if (!useLargeClock) {
-            largeClockTopMargin -=
-                context.resources.getDimensionPixelSize(
-                    com.android.systemui.customization.R.dimen.small_clock_height
-                )
-        }
-
-        return largeClockTopMargin
-    }
-
-    private fun getDimen(context: Context, name: String): Int {
-        val res = context.packageManager.getResourcesForApplication(context.packageName)
-        val id = res.getIdentifier(name, "dimen", context.packageName)
-        return res.getDimensionPixelSize(id)
-    }
-
-    companion object {
-        private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height"
-        private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height"
-    }
 }
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 5059e6b..5d36da9 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
@@ -46,6 +46,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
@@ -58,6 +59,8 @@
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
     aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+    lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
+    glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
     screenOffAnimationController: ScreenOffAnimationController,
     private val aodBurnInViewModel: AodBurnInViewModel,
     aodAlphaViewModel: AodAlphaViewModel,
@@ -78,7 +81,13 @@
         keyguardInteractor.notificationContainerBounds
 
     /** An observable for the alpha level for the entire keyguard root view. */
-    val alpha: Flow<Float> = aodAlphaViewModel.alpha
+    val alpha: Flow<Float> =
+        merge(
+                aodAlphaViewModel.alpha,
+                lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
+                glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
+            )
+            .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/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
index a1dd720..e8c1ab5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -33,6 +34,7 @@
     @Application applicationScope: CoroutineScope,
     smartspaceController: LockscreenSmartspaceController,
     keyguardClockViewModel: KeyguardClockViewModel,
+    smartspaceInteractor: KeyguardSmartspaceInteractor,
 ) {
     /** Whether the smartspace section is available in the build. */
     val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled()
@@ -78,4 +80,7 @@
     ): Boolean {
         return !clockIncludesCustomWeatherDisplay && isWeatherEnabled
     }
+
+    /* trigger clock and smartspace constraints change when smartspace appears */
+    var bcSmartspaceVisibility: StateFlow<Int> = smartspaceInteractor.bcSmartspaceVisibility
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 36bbe4e..d792889 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -16,9 +16,13 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.content.res.Resources
+import com.android.keyguard.KeyguardClockSwitch
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
@@ -31,12 +35,28 @@
 class LockscreenContentViewModel
 @Inject
 constructor(
+    clockInteractor: KeyguardClockInteractor,
     private val interactor: KeyguardBlueprintInteractor,
     private val authController: AuthController,
     val longPress: KeyguardLongPressViewModel,
 ) {
+    private val clockSize = clockInteractor.clockSize
+
     val isUdfpsVisible: Boolean
         get() = authController.isUdfpsSupported
+    val isLargeClockVisible: Boolean
+        get() = clockSize.value == KeyguardClockSwitch.LARGE
+    val areNotificationsVisible: Boolean
+        get() = !isLargeClockVisible
+
+    fun getSmartSpacePaddingTop(resources: Resources): Int {
+        return if (isLargeClockVisible) {
+            resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
+                resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
+        } else {
+            0
+        }
+    }
 
     fun blueprintId(scope: CoroutineScope): StateFlow<String> {
         return interactor.blueprint
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
new file mode 100644
index 0000000..3ea83ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+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 javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down LOCKSCREEN->GLANCEABLE_HUB transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class LockscreenToGlanceableHubTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            from = KeyguardState.LOCKSCREEN,
+            to = KeyguardState.GLANCEABLE_HUB,
+        )
+
+    // TODO(b/315205222): implement full animation spec instead of just a simple fade.
+    val keyguardAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+            onStep = { 1f - it },
+            onFinish = { 0f },
+            onCancel = { 1f },
+            name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha",
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index 6e7e099..bcd09bd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -18,23 +18,21 @@
 
 import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE
 import android.annotation.WorkerThread
-import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ResolveInfoFlags
 import android.os.UserHandle
 import android.service.quicksettings.TileService
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.data.repository.PackageChangeRepository
+import com.android.systemui.common.data.shared.model.PackageChangeModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.util.kotlin.isComponentActuallyEnabled
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
@@ -52,6 +50,7 @@
 constructor(
     @Application private val applicationContext: Context,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val packageChangeRepository: PackageChangeRepository
 ) : InstalledTilesComponentRepository {
 
     override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
@@ -70,24 +69,9 @@
                     )
                     .packageManager
             }
-        return conflatedCallbackFlow {
-                val receiver =
-                    object : BroadcastReceiver() {
-                        override fun onReceive(context: Context?, intent: Intent?) {
-                            trySend(Unit)
-                        }
-                    }
-                applicationContext.registerReceiverAsUser(
-                    receiver,
-                    UserHandle.of(userId),
-                    INTENT_FILTER,
-                    /* broadcastPermission = */ null,
-                    /* scheduler = */ null
-                )
-
-                awaitClose { applicationContext.unregisterReceiver(receiver) }
-            }
-            .onStart { emit(Unit) }
+        return packageChangeRepository
+            .packageChanged(UserHandle.of(userId))
+            .onStart { emit(PackageChangeModel.Empty) }
             .map { reloadComponents(userId, packageManager) }
             .distinctUntilChanged()
             .flowOn(backgroundDispatcher)
@@ -104,14 +88,6 @@
     }
 
     companion object {
-        private val INTENT_FILTER =
-            IntentFilter().apply {
-                addAction(Intent.ACTION_PACKAGE_ADDED)
-                addAction(Intent.ACTION_PACKAGE_CHANGED)
-                addAction(Intent.ACTION_PACKAGE_REMOVED)
-                addAction(Intent.ACTION_PACKAGE_REPLACED)
-                addDataScheme("package")
-            }
         private val INTENT = Intent(TileService.ACTION_QS_TILE)
         private val FLAGS =
             ResolveInfoFlags.of(
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
index 8c3e4a5..ab08f66 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
@@ -44,6 +44,7 @@
                 keyguardBottomAreaRefactor() &&
                 KeyguardShadeMigrationNssl.isEnabled &&
                 MediaInSceneContainerFlag.isEnabled &&
+                // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
                 ComposeFacade.isComposeAvailable()
 
     /**
@@ -63,6 +64,7 @@
             FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()),
             KeyguardShadeMigrationNssl.token,
             MediaInSceneContainerFlag.token,
+            // NOTE: Changes should also be made in isEnabled and @EnableSceneContainer
         )
 
     /** The full set of requirements for SceneContainer */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index cde2a62..8c852cd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -31,16 +31,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
-import com.android.keyguard.KeyguardMessageAreaController;
 import com.android.keyguard.LockIconViewController;
-import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
-import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
-import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder;
-import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
+import com.android.systemui.bouncer.ui.binder.BouncerViewBinder;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor;
@@ -56,8 +52,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.binder.AlternateBouncerViewBinder;
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies;
-import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
-import com.android.systemui.log.BouncerLogger;
 import com.android.systemui.res.R;
 import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
 import com.android.systemui.statusbar.DragDownHelper;
@@ -77,7 +71,6 @@
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.SystemClock;
 
@@ -183,24 +176,18 @@
             DumpManager dumpManager,
             PulsingGestureListener pulsingGestureListener,
             LockscreenHostedDreamGestureListener lockscreenHostedDreamGestureListener,
-            KeyguardBouncerViewModel keyguardBouncerViewModel,
-            KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
-            KeyguardMessageAreaController.Factory messageAreaControllerFactory,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
-            PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
             GlanceableHubContainerController glanceableHubContainerController,
             NotificationLaunchAnimationInteractor notificationLaunchAnimationInteractor,
             FeatureFlagsClassic featureFlagsClassic,
             SystemClock clock,
-            BouncerMessageInteractor bouncerMessageInteractor,
-            BouncerLogger bouncerLogger,
             SysUIKeyEventHandler sysUIKeyEventHandler,
             QuickSettingsController quickSettingsController,
             PrimaryBouncerInteractor primaryBouncerInteractor,
             AlternateBouncerInteractor alternateBouncerInteractor,
-            SelectedUserInteractor selectedUserInteractor,
             Lazy<JavaAdapter> javaAdapter,
-            Lazy<AlternateBouncerDependencies> alternateBouncerDependencies) {
+            Lazy<AlternateBouncerDependencies> alternateBouncerDependencies,
+            BouncerViewBinder bouncerViewBinder) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
         mStatusBarStateController = statusBarStateController;
@@ -234,15 +221,7 @@
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
         mDisableSubpixelTextTransitionListener = new DisableSubpixelTextTransitionListener(mView);
-        KeyguardBouncerViewBinder.bind(
-                mView.findViewById(R.id.keyguard_bouncer_container),
-                keyguardBouncerViewModel,
-                primaryBouncerToGoneTransitionViewModel,
-                keyguardBouncerComponentFactory,
-                messageAreaControllerFactory,
-                bouncerMessageInteractor,
-                bouncerLogger,
-                selectedUserInteractor);
+        bouncerViewBinder.bind(mView.findViewById(R.id.keyguard_bouncer_container));
 
         if (DeviceEntryUdfpsRefactor.isEnabled()) {
             AlternateBouncerViewBinder.bind(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
index ec10aaf..88e94e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
@@ -22,12 +22,17 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import javax.inject.Inject
 
 /** pipeline-agnostic implementation for getting [NotificationVisibility]. */
 @SysUISingleton
-class NotificationVisibilityProviderImpl @Inject constructor(
+class NotificationVisibilityProviderImpl
+@Inject
+constructor(
+    private val activeNotificationsInteractor: ActiveNotificationsInteractor,
     private val notifDataStore: NotifLiveDataStore,
     private val notifCollection: CommonNotifCollection
 ) : NotificationVisibilityProvider {
@@ -47,5 +52,10 @@
     override fun getLocation(key: String): NotificationVisibility.NotificationLocation =
         NotificationLogger.getNotificationLocation(notifCollection.getEntry(key))
 
-    private fun getCount() = notifDataStore.activeNotifCount.value
+    private fun getCount() =
+        if (NotificationsLiveDataStoreRefactor.isEnabled) {
+            activeNotificationsInteractor.allNotificationsCountValue
+        } else {
+            notifDataStore.activeNotifCount.value
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt
new file mode 100644
index 0000000..cbd9887
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.dagger
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.NoOpCoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.statusbar.NotificationListener
+import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider
+import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.logging.NotificationLogger.ExpansionStateLogger
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLoggerImpl
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLoggerViewModel
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.kotlin.getOrNull
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.util.Optional
+import java.util.concurrent.Executor
+import javax.inject.Provider
+
+@Module
+interface NotificationStatsLoggerModule {
+
+    /** Binds an implementation to the [NotificationStatsLogger]. */
+    @Binds fun bindsStatsLoggerImpl(impl: NotificationStatsLoggerImpl): NotificationStatsLogger
+
+    companion object {
+
+        /** Provides a [NotificationStatsLogger] if the refactor flag is on. */
+        @Provides
+        fun provideStatsLogger(
+            provider: Provider<NotificationStatsLogger>
+        ): Optional<NotificationStatsLogger> {
+            return if (NotificationsLiveDataStoreRefactor.isEnabled) {
+                Optional.of(provider.get())
+            } else {
+                Optional.empty()
+            }
+        }
+
+        /** Provides a [NotificationLoggerViewModel] if the refactor flag is on. */
+        @Provides
+        fun provideViewModel(
+            provider: Provider<NotificationLoggerViewModel>
+        ): Optional<NotificationLoggerViewModel> {
+            return if (NotificationsLiveDataStoreRefactor.isEnabled) {
+                Optional.of(provider.get())
+            } else {
+                Optional.empty()
+            }
+        }
+
+        /** Provides the legacy [NotificationLogger] if the refactor flag is off. */
+        @Provides
+        @SysUISingleton
+        fun provideLegacyLoggerOptional(
+            notificationListener: NotificationListener?,
+            @UiBackground uiBgExecutor: Executor?,
+            notifLiveDataStore: NotifLiveDataStore?,
+            visibilityProvider: NotificationVisibilityProvider?,
+            notifPipeline: NotifPipeline?,
+            statusBarStateController: StatusBarStateController?,
+            windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor?,
+            javaAdapter: JavaAdapter?,
+            expansionStateLogger: ExpansionStateLogger?,
+            notificationPanelLogger: NotificationPanelLogger?
+        ): Optional<NotificationLogger> {
+            return if (NotificationsLiveDataStoreRefactor.isEnabled) {
+                Optional.empty()
+            } else {
+                Optional.of(
+                    NotificationLogger(
+                        notificationListener,
+                        uiBgExecutor,
+                        notifLiveDataStore,
+                        visibilityProvider,
+                        notifPipeline,
+                        statusBarStateController,
+                        windowRootViewVisibilityInteractor,
+                        javaAdapter,
+                        expansionStateLogger,
+                        notificationPanelLogger
+                    )
+                )
+            }
+        }
+
+        /**
+         * Provides a the legacy [NotificationLogger] or the new [NotificationStatsLogger] to the
+         * notification row.
+         *
+         * TODO(b/308623704) remove the [NotificationRowStatsLogger] interface, and provide a
+         *   [NotificationStatsLogger] to the row directly.
+         */
+        @Provides
+        fun provideRowStatsLogger(
+            newProvider: Provider<NotificationStatsLogger>,
+            legacyLoggerOptional: Optional<NotificationLogger>,
+        ): NotificationRowStatsLogger {
+            return legacyLoggerOptional.getOrNull() ?: newProvider.get()
+        }
+
+        /**
+         * Binds the legacy [NotificationLogger] as a [CoreStartable] if the feature flag is off, or
+         * binds a no-op [CoreStartable] otherwise.
+         *
+         * The old [NotificationLogger] is a [CoreStartable], because it's managing its own data
+         * updates, but the new [NotificationStatsLogger] is not. Currently Dagger doesn't support
+         * optionally binding entries with @[IntoMap], therefore we provide a no-op [CoreStartable]
+         * here if the feature flag is on, but this can be removed once the flag is released.
+         */
+        @Provides
+        @IntoMap
+        @ClassKey(NotificationLogger::class)
+        fun provideCoreStartable(
+            legacyLoggerOptional: Optional<NotificationLogger>
+        ): CoreStartable {
+            return legacyLoggerOptional.getOrNull() ?: NoOpCoreStartable()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 3a72205..6bba72b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -17,14 +17,12 @@
 package com.android.systemui.statusbar.notification.dagger;
 
 import android.content.Context;
+import android.service.notification.NotificationListenerService;
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.UiBackground;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
-import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
@@ -67,7 +65,6 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider;
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProviderImpl;
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
 import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -80,7 +77,8 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
-import com.android.systemui.util.kotlin.JavaAdapter;
+
+import javax.inject.Provider;
 
 import dagger.Binds;
 import dagger.Module;
@@ -88,10 +86,6 @@
 import dagger.multibindings.ClassKey;
 import dagger.multibindings.IntoMap;
 
-import java.util.concurrent.Executor;
-
-import javax.inject.Provider;
-
 /**
  * Dagger Module for classes found within the com.android.systemui.statusbar.notification package.
  */
@@ -104,6 +98,7 @@
         NotificationSectionHeadersModule.class,
         ActivatableNotificationViewModelModule.class,
         NotificationMemoryModule.class,
+        NotificationStatsLoggerModule.class,
 })
 public interface NotificationsModule {
     @Binds
@@ -128,39 +123,6 @@
     VisibilityLocationProvider bindVisibilityLocationProvider(
             VisibilityLocationProviderDelegator visibilityLocationProviderDelegator);
 
-    /** Provides an instance of {@link NotificationLogger} */
-    @SysUISingleton
-    @Provides
-    static NotificationLogger provideNotificationLogger(
-            NotificationListener notificationListener,
-            @UiBackground Executor uiBgExecutor,
-            NotifLiveDataStore notifLiveDataStore,
-            NotificationVisibilityProvider visibilityProvider,
-            NotifPipeline notifPipeline,
-            StatusBarStateController statusBarStateController,
-            WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor,
-            JavaAdapter javaAdapter,
-            NotificationLogger.ExpansionStateLogger expansionStateLogger,
-            NotificationPanelLogger notificationPanelLogger) {
-        return new NotificationLogger(
-                notificationListener,
-                uiBgExecutor,
-                notifLiveDataStore,
-                visibilityProvider,
-                notifPipeline,
-                statusBarStateController,
-                windowRootViewVisibilityInteractor,
-                javaAdapter,
-                expansionStateLogger,
-                notificationPanelLogger);
-    }
-
-    /** Binds {@link NotificationLogger} as a {@link CoreStartable}. */
-    @Binds
-    @IntoMap
-    @ClassKey(NotificationLogger.class)
-    CoreStartable bindsNotificationLogger(NotificationLogger notificationLogger);
-
     /** Provides an instance of {@link NotificationPanelLogger} */
     @SysUISingleton
     @Provides
@@ -272,6 +234,10 @@
     NotifLiveDataStore bindNotifLiveDataStore(NotifLiveDataStoreImpl notifLiveDataStoreImpl);
 
     /** */
+    @Binds
+    NotificationListenerService bindNotificationListener(NotificationListener notificationListener);
+
+    /** */
     @Provides
     @SysUISingleton
     static VisualInterruptionDecisionProvider provideVisualInterruptionDecisionProvider(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
index 5ed82cc..5c844bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
@@ -55,6 +55,12 @@
      * invoking [get].
      */
     val renderList: List<Key> = emptyList(),
+
+    /**
+     * Map of notification key to rank, where rank is the 0-based index of the notification on the
+     * system server, meaning that in the unfiltered flattened list of notification entries.
+     */
+    val rankingsMap: Map<String, Int> = emptyMap()
 ) {
     operator fun get(key: Key): ActiveNotificationEntryModel? {
         return when (key) {
@@ -74,8 +80,9 @@
         private val groups = mutableMapOf<String, ActiveNotificationGroupModel>()
         private val individuals = mutableMapOf<String, ActiveNotificationModel>()
         private val renderList = mutableListOf<Key>()
+        private var rankingsMap: Map<String, Int> = emptyMap()
 
-        fun build() = ActiveNotificationsStore(groups, individuals, renderList)
+        fun build() = ActiveNotificationsStore(groups, individuals, renderList, rankingsMap)
 
         fun addEntry(entry: ActiveNotificationEntryModel) {
             when (entry) {
@@ -95,5 +102,9 @@
             individuals[group.summary.key] = group.summary
             group.children.forEach { individuals[it.key] = it }
         }
+
+        fun setRankingsMap(map: Map<String, Int>) {
+            rankingsMap = map.toMap()
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index e90ddf9..5180bab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -33,7 +33,10 @@
     private val repository: ActiveNotificationListRepository,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) {
-    /** Notifications actively presented to the user in the notification stack, in order. */
+    /**
+     * Top level list of Notifications actively presented to the user in the notification stack, in
+     * order.
+     */
     val topLevelRepresentativeNotifications: Flow<List<ActiveNotificationModel>> =
         repository.activeNotifications
             .map { store ->
@@ -51,6 +54,22 @@
             }
             .flowOn(backgroundDispatcher)
 
+    /**
+     * Flattened list of Notifications actively presented to the user in the notification stack, in
+     * order.
+     */
+    val allRepresentativeNotifications: Flow<Map<String, ActiveNotificationModel>> =
+        repository.activeNotifications.map { store -> store.individuals }
+
+    /** Size of the flattened list of Notifications actively presented in the stack. */
+    val allNotificationsCount: Flow<Int> =
+        repository.activeNotifications.map { store -> store.individuals.size }
+
+    /**
+     * The same as [allNotificationsCount], but without flows, for easy access in synchronous code.
+     */
+    val allNotificationsCountValue: Int = repository.activeNotifications.value.individuals.size
+
     /** Are any notifications being actively presented in the notification stack? */
     val areAnyNotificationsPresent: Flow<Boolean> =
         repository.activeNotifications
@@ -65,6 +84,16 @@
     val areAnyNotificationsPresentValue: Boolean
         get() = repository.activeNotifications.value.renderList.isNotEmpty()
 
+    /**
+     * Map of notification key to rank, where rank is the 0-based index of the notification in the
+     * system server, meaning that in the unfiltered flattened list of notification entries. Used
+     * for logging purposes.
+     *
+     * @see [com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger].
+     */
+    val activeNotificationRanks: Flow<Map<String, Int>> =
+        repository.activeNotifications.map { store -> store.rankingsMap }
+
     /** Are there are any notifications that can be cleared by the "Clear all" button? */
     val hasClearableNotifications: Flow<Boolean> =
         repository.notifStats
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index 6f4ed9d..695f215 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.statusbar.notification.domain.interactor
 
 import android.graphics.drawable.Icon
+import android.util.ArrayMap
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -46,6 +47,7 @@
         repository.activeNotifications.update { existingModels ->
             buildActiveNotificationsStore(existingModels, sectionStyleProvider) {
                 entries.forEach(::addListEntry)
+                setRankingsMap(entries)
             }
         }
     }
@@ -94,6 +96,27 @@
         }
     }
 
+    fun setRankingsMap(entries: List<ListEntry>) {
+        builder.setRankingsMap(flatMapToRankingsMap(entries))
+    }
+
+    fun flatMapToRankingsMap(entries: List<ListEntry>): Map<String, Int> {
+        val result = ArrayMap<String, Int>()
+        for (entry in entries) {
+            if (entry is NotificationEntry) {
+                entry.representativeEntry?.let { representativeEntry ->
+                    result[representativeEntry.key] = representativeEntry.ranking.rank
+                }
+            } else if (entry is GroupEntry) {
+                entry.summary?.let { summary -> result[summary.key] = summary.ranking.rank }
+                for (child in entry.children) {
+                    result[child.key] = child.ranking.rank
+                }
+            }
+        }
+        return result
+    }
+
     private fun NotificationEntry.toModel(): ActiveNotificationModel =
         existingModels.createOrReuse(
             key = key,
@@ -107,6 +130,11 @@
             aodIcon = icons.aodIcon?.sourceIcon,
             shelfIcon = icons.shelfIcon?.sourceIcon,
             statusBarIcon = icons.statusBarIcon?.sourceIcon,
+            uid = sbn.uid,
+            packageName = sbn.packageName,
+            instanceId = sbn.instanceId?.id,
+            isGroupSummary = sbn.notification.isGroupSummary,
+            bucket = bucket,
         )
 }
 
@@ -121,7 +149,12 @@
     isPulsing: Boolean,
     aodIcon: Icon?,
     shelfIcon: Icon?,
-    statusBarIcon: Icon?
+    statusBarIcon: Icon?,
+    uid: Int,
+    packageName: String,
+    instanceId: Int?,
+    isGroupSummary: Boolean,
+    bucket: Int,
 ): ActiveNotificationModel {
     return individuals[key]?.takeIf {
         it.isCurrent(
@@ -135,7 +168,12 @@
             isPulsing = isPulsing,
             aodIcon = aodIcon,
             shelfIcon = shelfIcon,
-            statusBarIcon = statusBarIcon
+            statusBarIcon = statusBarIcon,
+            uid = uid,
+            instanceId = instanceId,
+            isGroupSummary = isGroupSummary,
+            packageName = packageName,
+            bucket = bucket,
         )
     }
         ?: ActiveNotificationModel(
@@ -150,6 +188,11 @@
             aodIcon = aodIcon,
             shelfIcon = shelfIcon,
             statusBarIcon = statusBarIcon,
+            uid = uid,
+            instanceId = instanceId,
+            isGroupSummary = isGroupSummary,
+            packageName = packageName,
+            bucket = bucket,
         )
 }
 
@@ -164,7 +207,12 @@
     isPulsing: Boolean,
     aodIcon: Icon?,
     shelfIcon: Icon?,
-    statusBarIcon: Icon?
+    statusBarIcon: Icon?,
+    uid: Int,
+    packageName: String,
+    instanceId: Int?,
+    isGroupSummary: Boolean,
+    bucket: Int,
 ): Boolean {
     return when {
         key != this.key -> false
@@ -178,6 +226,11 @@
         aodIcon != this.aodIcon -> false
         shelfIcon != this.shelfIcon -> false
         statusBarIcon != this.statusBarIcon -> false
+        uid != this.uid -> false
+        instanceId != this.instanceId -> false
+        isGroupSummary != this.isGroupSummary -> false
+        packageName != this.packageName -> false
+        bucket != this.bucket -> false
         else -> true
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 7d1cca8..ac49960 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
 import com.android.systemui.statusbar.notification.logging.NotificationLogger
 import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import com.android.wm.shell.bubbles.Bubbles
 import dagger.Lazy
@@ -53,7 +54,9 @@
  * any initialization work that notifications require.
  */
 @SysUISingleton
-class NotificationsControllerImpl @Inject constructor(
+class NotificationsControllerImpl
+@Inject
+constructor(
     private val notificationListener: NotificationListener,
     private val commonNotifCollection: Lazy<CommonNotifCollection>,
     private val notifPipeline: Lazy<NotifPipeline>,
@@ -61,7 +64,7 @@
     private val targetSdkResolver: TargetSdkResolver,
     private val notifPipelineInitializer: Lazy<NotifPipelineInitializer>,
     private val notifBindPipelineInitializer: NotifBindPipelineInitializer,
-    private val notificationLogger: NotificationLogger,
+    private val notificationLoggerOptional: Optional<NotificationLogger>,
     private val notificationRowBinder: NotificationRowBinderImpl,
     private val notificationsMediaManager: NotificationMediaManager,
     private val headsUpViewBinder: HeadsUpViewBinder,
@@ -80,28 +83,35 @@
     ) {
         notificationListener.registerAsSystemService()
 
-        notifPipeline.get().addCollectionListener(object : NotifCollectionListener {
-            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
-                listContainer.cleanUpViewStateForEntry(entry)
-            }
-        })
+        notifPipeline
+            .get()
+            .addCollectionListener(
+                object : NotifCollectionListener {
+                    override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+                        listContainer.cleanUpViewStateForEntry(entry)
+                    }
+                }
+            )
 
         notificationRowBinder.setNotificationClicker(
-            clickerBuilder.build(bubblesOptional, notificationActivityStarter))
+            clickerBuilder.build(bubblesOptional, notificationActivityStarter)
+        )
         notificationRowBinder.setUpWithPresenter(presenter, listContainer)
         headsUpViewBinder.setPresenter(presenter)
         notifBindPipelineInitializer.initialize()
         animatedImageNotificationManager.bind()
 
-        notifPipelineInitializer.get().initialize(
-                notificationListener,
-                notificationRowBinder,
-                listContainer,
-                stackController)
+        notifPipelineInitializer
+            .get()
+            .initialize(notificationListener, notificationRowBinder, listContainer, stackController)
 
         targetSdkResolver.initialize(notifPipeline.get())
         notificationsMediaManager.setUpWithPresenter(presenter)
-        notificationLogger.setUpWithContainer(listContainer)
+        if (!NotificationsLiveDataStoreRefactor.isEnabled) {
+            notificationLoggerOptional.ifPresent { logger ->
+                logger.setUpWithContainer(listContainer)
+            }
+        }
         peopleSpaceWidgetManager.attach(notificationListener)
     }
 
@@ -120,11 +130,11 @@
             notificationListener.snoozeNotification(sbn.key, snoozeOption.snoozeCriterion.id)
         } else {
             notificationListener.snoozeNotification(
-                    sbn.key,
-                    snoozeOption.minutesToSnoozeFor * 60 * 1000.toLong())
+                sbn.key,
+                snoozeOption.minutesToSnoozeFor * 60 * 1000.toLong()
+            )
         }
     }
 
-    override fun getActiveNotificationsCount(): Int =
-        notifLiveDataStore.activeNotifCount.value
+    override fun getActiveNotificationsCount(): Int = notifLiveDataStore.activeNotifCount.value
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 8d2a63e..4349b3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -33,6 +33,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
+import com.android.internal.statusbar.NotificationVisibility.NotificationLocation;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -46,8 +47,10 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger;
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.kotlin.JavaAdapter;
 
@@ -64,7 +67,8 @@
  * Handles notification logging, in particular, logging which notifications are visible and which
  * are not.
  */
-public class NotificationLogger implements StateListener, CoreStartable {
+public class NotificationLogger implements StateListener, CoreStartable,
+        NotificationRowStatsLogger {
     static final String TAG = "NotificationLogger";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
 
@@ -166,31 +170,31 @@
     /**
      * Returns the location of the notification referenced by the given {@link NotificationEntry}.
      */
-    public static NotificationVisibility.NotificationLocation getNotificationLocation(
+    public static NotificationLocation getNotificationLocation(
             NotificationEntry entry) {
         if (entry == null || entry.getRow() == null || entry.getRow().getViewState() == null) {
-            return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN;
+            return NotificationLocation.LOCATION_UNKNOWN;
         }
         return convertNotificationLocation(entry.getRow().getViewState().location);
     }
 
-    private static NotificationVisibility.NotificationLocation convertNotificationLocation(
+    private static NotificationLocation convertNotificationLocation(
             int location) {
         switch (location) {
             case ExpandableViewState.LOCATION_FIRST_HUN:
-                return NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP;
+                return NotificationLocation.LOCATION_FIRST_HEADS_UP;
             case ExpandableViewState.LOCATION_HIDDEN_TOP:
-                return NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP;
+                return NotificationLocation.LOCATION_HIDDEN_TOP;
             case ExpandableViewState.LOCATION_MAIN_AREA:
-                return NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA;
+                return NotificationLocation.LOCATION_MAIN_AREA;
             case ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING:
-                return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING;
+                return NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING;
             case ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN:
-                return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN;
+                return NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN;
             case ExpandableViewState.LOCATION_GONE:
-                return NotificationVisibility.NotificationLocation.LOCATION_GONE;
+                return NotificationLocation.LOCATION_GONE;
             default:
-                return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN;
+                return NotificationLocation.LOCATION_UNKNOWN;
         }
     }
 
@@ -207,6 +211,9 @@
             JavaAdapter javaAdapter,
             ExpansionStateLogger expansionStateLogger,
             NotificationPanelLogger notificationPanelLogger) {
+        // Not expected to be constructed if the feature flag is on
+        NotificationsLiveDataStoreRefactor.assertInLegacyMode();
+
         mNotificationListener = notificationListener;
         mUiBgExecutor = uiBgExecutor;
         mNotifLiveDataStore = notifLiveDataStore;
@@ -382,9 +389,11 @@
     /**
      * Called when the notification is expanded / collapsed.
      */
-    public void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded) {
-        NotificationVisibility.NotificationLocation location = mVisibilityProvider.getLocation(key);
-        mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, location);
+    @Override
+    public void onNotificationExpansionChanged(@NonNull String key, boolean isExpanded,
+            int location, boolean isUserAction) {
+        NotificationLocation notifLocation = mVisibilityProvider.getLocation(key);
+        mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, notifLocation);
     }
 
     @VisibleForTesting
@@ -440,7 +449,7 @@
 
         @VisibleForTesting
         void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded,
-                NotificationVisibility.NotificationLocation location) {
+                NotificationLocation location) {
             State state = getState(key);
             state.mIsUserAction = isUserAction;
             state.mIsExpanded = isExpanded;
@@ -528,7 +537,7 @@
             @Nullable
             Boolean mIsVisible;
             @Nullable
-            NotificationVisibility.NotificationLocation mLocation;
+            NotificationLocation mLocation;
 
             private State() {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
index 5ca13c9..6c63d1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
@@ -40,6 +40,11 @@
 
     /**
      * Log a NOTIFICATION_PANEL_REPORTED statsd event.
+     */
+    void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto);
+
+    /**
+     * Log a NOTIFICATION_PANEL_REPORTED statsd event.
      * @param visibleNotifications as provided by NotificationEntryManager.getVisibleNotifications()
      */
     void logPanelShown(boolean isLockscreen,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
index 9a63228..d7f7b76 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
@@ -21,6 +21,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.nano.Notifications;
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
 
 import com.google.protobuf.nano.MessageNano;
 
@@ -31,15 +32,25 @@
  * Normal implementation of NotificationPanelLogger.
  */
 public class NotificationPanelLoggerImpl implements NotificationPanelLogger {
+
+    @Override
+    public void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto) {
+        SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
+                /* event_id = */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(),
+                /* num_notifications = */ proto.notifications.length,
+                /* notifications = */ MessageNano.toByteArray(proto));
+    }
+
     @Override
     public void logPanelShown(boolean isLockscreen,
             List<NotificationEntry> visibleNotifications) {
+        NotificationsLiveDataStoreRefactor.assertInLegacyMode();
         final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto(
                 visibleNotifications);
         SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
-                /* int event_id */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(),
-                /* int num_notifications*/ proto.notifications.length,
-                /* byte[] notifications*/ MessageNano.toByteArray(proto));
+                /* event_id = */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(),
+                /* num_notifications = */ proto.notifications.length,
+                /* notifications = */ MessageNano.toByteArray(proto));
     }
 
     @Override
@@ -47,8 +58,8 @@
         final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto(
                 Collections.singletonList(draggedNotification));
         SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
-                /* int event_id */ NOTIFICATION_DRAG.getId(),
-                /* int num_notifications*/ proto.notifications.length,
-                /* byte[] notifications*/ MessageNano.toByteArray(proto));
+                /* event_id = */ NOTIFICATION_DRAG.getId(),
+                /* num_notifications = */ proto.notifications.length,
+                /* notifications = */ MessageNano.toByteArray(proto));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index e200e65..5eeb066 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -118,6 +118,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BooleanSupplier;
@@ -988,6 +989,25 @@
     }
 
     /**
+     * Recursively collects the [{@link ExpandableViewState#location}]s populating the provided
+     * map.
+     * The visibility of each child is determined by the {@link View#getVisibility()}.
+     * Locations are added to the provided map including locations from child views, that are
+     * visible.
+     */
+    public void collectVisibleLocations(Map<String, Integer> locationsMap) {
+        if (getVisibility() == View.VISIBLE) {
+            locationsMap.put(getEntry().getKey(), getViewState().location);
+            if (mChildrenContainer != null) {
+                List<ExpandableNotificationRow> children = mChildrenContainer.getAttachedChildren();
+                for (int i = 0; i < children.size(); i++) {
+                    children.get(i).collectVisibleLocations(locationsMap);
+                }
+            }
+        }
+    }
+
+    /**
      * Updates states of all children.
      */
     public void updateChildrenStates() {
@@ -1615,7 +1635,8 @@
         /**
          * Called when the notification is expanded / collapsed.
          */
-        void logNotificationExpansion(String key, boolean userAction, boolean expanded);
+        void logNotificationExpansion(String key, int location, boolean userAction,
+                boolean expanded);
 
         /**
          * Called when a notification which was previously kept in its parent for the
@@ -3312,7 +3333,8 @@
         if (nowExpanded != wasExpanded) {
             updateShelfIconColor();
             if (mLogger != null) {
-                mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded);
+                mLogger.logNotificationExpansion(mLoggingKey, getViewState().location, userAction,
+                        nowExpanded);
             }
             if (mIsSummaryWithChildren) {
                 mChildrenContainer.onExpansionChanged();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index af55f44..0afdefa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -48,13 +48,13 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.collection.render.NodeController;
 import com.android.systemui.statusbar.notification.collection.render.NotifViewController;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
 import com.android.systemui.statusbar.notification.row.dagger.AppName;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationKey;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope;
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.SmartReplyConstants;
@@ -90,7 +90,7 @@
     private final GroupMembershipManager mGroupMembershipManager;
     private final GroupExpansionManager mGroupExpansionManager;
     private final RowContentBindStage mRowContentBindStage;
-    private final NotificationLogger mNotificationLogger;
+    private final NotificationRowStatsLogger mStatsLogger;
     private final NotificationRowLogger mLogBufferLogger;
     private final HeadsUpManager mHeadsUpManager;
     private final ExpandableNotificationRow.OnExpandClickListener mOnExpandClickListener;
@@ -130,9 +130,10 @@
     private final ExpandableNotificationRow.ExpandableNotificationRowLogger mLoggerCallback =
             new ExpandableNotificationRow.ExpandableNotificationRowLogger() {
                 @Override
-                public void logNotificationExpansion(String key, boolean userAction,
+                public void logNotificationExpansion(String key, int location, boolean userAction,
                         boolean expanded) {
-                    mNotificationLogger.onExpansionChanged(key, userAction, expanded);
+                    mStatsLogger.onNotificationExpansionChanged(key, expanded, location,
+                            userAction);
                 }
 
                 @Override
@@ -212,7 +213,7 @@
             GroupMembershipManager groupMembershipManager,
             GroupExpansionManager groupExpansionManager,
             RowContentBindStage rowContentBindStage,
-            NotificationLogger notificationLogger,
+            NotificationRowStatsLogger statsLogger,
             HeadsUpManager headsUpManager,
             ExpandableNotificationRow.OnExpandClickListener onExpandClickListener,
             StatusBarStateController statusBarStateController,
@@ -239,7 +240,7 @@
         mGroupMembershipManager = groupMembershipManager;
         mGroupExpansionManager = groupExpansionManager;
         mRowContentBindStage = rowContentBindStage;
-        mNotificationLogger = notificationLogger;
+        mStatsLogger = statsLogger;
         mHeadsUpManager = headsUpManager;
         mOnExpandClickListener = onExpandClickListener;
         mStatusBarStateController = statusBarStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 6528cef3..a1718b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -37,6 +37,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -45,8 +46,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.TransformableView;
@@ -898,6 +899,7 @@
         // forceUpdateVisibilities cancels outstanding animations without updating the
         // mAnimationStartVisibleType. Do so here instead.
         mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
+        notifySubtreeForAccessibilityContentChange();
     }
 
     private void fireExpandedVisibleListenerIfVisible() {
@@ -980,6 +982,7 @@
         // updateViewVisibilities cancels outstanding animations without updating the
         // mAnimationStartVisibleType. Do so here instead.
         mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
+        notifySubtreeForAccessibilityContentChange();
     }
 
     private void updateViewVisibility(int visibleType, int type, View view,
@@ -1029,6 +1032,7 @@
                     hiddenView.setVisible(false);
                 }
                 mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
+                notifySubtreeForAccessibilityContentChange();
             }
         });
         fireExpandedVisibleListenerIfVisible();
@@ -1049,6 +1053,22 @@
         }
     }
 
+    @Override
+    public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
+        if (isAnimatingVisibleType()) {
+            // Don't send A11y events while animating to reduce Jank.
+            return;
+        }
+        super.notifySubtreeAccessibilityStateChanged(child, source, changeType);
+    }
+
+    private void notifySubtreeForAccessibilityContentChange() {
+        if (mParent != null) {
+            mParent.notifySubtreeAccessibilityStateChanged(this, this,
+                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+        }
+    }
+
     /**
      * @param visibleType one of the static enum types in this view
      * @return the corresponding transformable view according to the given visible type
@@ -2277,6 +2297,11 @@
         mHeadsUpWrapper = headsUpWrapper;
     }
 
+    @VisibleForTesting
+    protected void setAnimationStartVisibleType(int animationStartVisibleType) {
+        mAnimationStartVisibleType = animationStartVisibleType;
+    }
+
     @Override
     protected void dispatchDraw(Canvas canvas) {
         try {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index eb1c1ba..5527efc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.statusbar.notification.shared
 
 import android.graphics.drawable.Icon
+import com.android.systemui.statusbar.notification.stack.PriorityBucket
 
 /**
  * Model for a top-level "entry" in the notification list, either an
@@ -55,6 +56,16 @@
     val shelfIcon: Icon?,
     /** Icon to display in the status bar. */
     val statusBarIcon: Icon?,
+    /** The notifying app's [packageName]'s uid. */
+    val uid: Int,
+    /** The notifying app's packageName. */
+    val packageName: String,
+    /** A small per-notification ID, used for statsd logging. */
+    val instanceId: Int?,
+    /** If this notification is the group summary for a group of notifications. */
+    val isGroupSummary: Boolean,
+    /** Indicates in which section the notification is displayed in. @see [PriorityBucket]. */
+    @PriorityBucket val bucket: Int,
 ) : ActiveNotificationEntryModel()
 
 /** Model for a group of notifications. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt
new file mode 100644
index 0000000..a1fb983
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import javax.inject.Inject
+
+/**
+ * Tracks latencies related to temporary hiding notifications while measuring
+ * them, which is an optimization to show some content as early as possible
+ * and perform notifications measurement later.
+ * See [HideNotificationsInteractor].
+ */
+class DisplaySwitchNotificationsHiderTracker @Inject constructor(
+    private val notificationsInteractor: ShadeInteractor,
+    private val latencyTracker: LatencyTracker
+) {
+
+    suspend fun trackNotificationHideTime(shouldHideNotifications: Flow<Boolean>) {
+        shouldHideNotifications
+            .collect { shouldHide ->
+                if (shouldHide) {
+                    latencyTracker.onActionStart(ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE)
+                } else {
+                    latencyTracker.onActionEnd(ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE)
+                }
+            }
+    }
+
+    suspend fun trackNotificationHideTimeWhenVisible(shouldHideNotifications: Flow<Boolean>) {
+        combine(shouldHideNotifications, notificationsInteractor.isAnyExpanded)
+            { hidden, shadeExpanded -> hidden && shadeExpanded }
+            .distinctUntilChanged()
+            .collect { hiddenButShouldBeVisible ->
+                if (hiddenButShouldBeVisible) {
+                    latencyTracker.onActionStart(
+                            ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN)
+                } else {
+                    latencyTracker.onActionEnd(
+                            ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN)
+                }
+            }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
index 6bb9573..5c9a0b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
@@ -23,7 +23,6 @@
 
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
-import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index ea414d2..e791a64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -113,6 +113,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -129,9 +130,12 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
@@ -245,6 +249,7 @@
      */
     private float mOverScrolledBottomPixels;
     private NotificationLogger.OnChildLocationsChangedListener mListener;
+    private OnNotificationLocationsChangedListener mLocationsChangedListener;
     private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
     private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
     private Runnable mOnHeightChangedRunnable;
@@ -390,6 +395,14 @@
         }
     };
 
+    private final Callable<Map<String, Integer>> collectVisibleLocationsCallable =
+            new Callable<>() {
+                @Override
+                public Map<String, Integer> call() {
+                    return collectVisibleNotificationLocations();
+                }
+            };
+
     private boolean mPulsing;
     private boolean mScrollable;
     private View mForcedScroll;
@@ -1242,8 +1255,21 @@
         }
     }
 
+    /**
+     * @param listener to be notified after the location of Notification children might have
+     *                 changed.
+     */
+    public void setNotificationLocationsChangedListener(
+            @Nullable OnNotificationLocationsChangedListener listener) {
+        if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) {
+            return;
+        }
+        mLocationsChangedListener = listener;
+    }
+
     public void setChildLocationsChangedListener(
             NotificationLogger.OnChildLocationsChangedListener listener) {
+        NotificationsLiveDataStoreRefactor.assertInLegacyMode();
         mListener = listener;
     }
 
@@ -4398,15 +4424,40 @@
             child.applyViewState();
         }
 
-        if (mListener != null) {
-            mListener.onChildLocationsChanged();
+        if (NotificationsLiveDataStoreRefactor.isEnabled()) {
+            if (mLocationsChangedListener != null) {
+                mLocationsChangedListener.onChildLocationsChanged(collectVisibleLocationsCallable);
+            }
+        } else {
+            if (mListener != null) {
+                mListener.onChildLocationsChanged();
+            }
         }
+
         runAnimationFinishedRunnables();
         setAnimationRunning(false);
         updateBackground();
         updateViewShadows();
     }
 
+    /**
+     * Retrieves a map of visible [{@link ExpandableViewState#location}]s of the actively displayed
+     * Notification children associated by their Notification keys.
+     * Locations are collected recursively including locations from the child views of Notification
+     * Groups, that are visible.
+     */
+    private Map<String, Integer> collectVisibleNotificationLocations() {
+        Map<String, Integer> visibilities = new HashMap<>();
+        int numChildren = getChildCount();
+        for (int i = 0; i < numChildren; i++) {
+            ExpandableView child = getChildAtIndex(i);
+            if (child instanceof ExpandableNotificationRow row) {
+                row.collectVisibleLocations(visibilities);
+            }
+        }
+        return visibilities;
+    }
+
     private void updateViewShadows() {
         // we need to work around an issue where the shadow would not cast between siblings when
         // their z difference is between 0 and 0.1
@@ -5901,7 +5952,11 @@
         }
     }
 
-    protected void setLogger(StackStateLogger logger) {
+    /**
+     * Sets a {@link StackStateLogger} which is notified as the {@link StackStateAnimator} updates
+     * the views.
+     */
+    protected void setStackStateLogger(StackStateLogger logger) {
         mStateAnimator.setLogger(logger);
     }
 
@@ -5938,6 +5993,18 @@
         void flingTopOverscroll(float velocity, boolean open);
     }
 
+    /**
+     * A listener that is notified when some ExpandableNotificationRow locations might have changed.
+     */
+    public interface OnNotificationLocationsChangedListener {
+        /**
+         * Called when the location of ExpandableNotificationRows might have changed.
+         *
+         * @param locations mapping of Notification keys to locations.
+         */
+        void onChildLocationsChanged(Callable<Map<String, Integer>> locations);
+    }
+
     private void updateSpeedBumpIndex() {
         mSpeedBumpIndexDirty = true;
     }
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 a30c294..c0e0b73 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
@@ -751,7 +751,7 @@
     }
 
     private void setUpView() {
-        mView.setLogger(mStackStateLogger);
+        mView.setStackStateLogger(mStackStateLogger);
         mView.setController(this);
         mView.setLogger(mLogger);
         mView.setTouchHandler(new TouchHandler());
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt
similarity index 70%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt
index ce92b6d..2305c7e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt
@@ -13,10 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+interface NotificationRowStatsLogger {
+    fun onNotificationExpansionChanged(
+        key: String,
+        isExpanded: Boolean,
+        location: Int,
+        isUserAction: Boolean
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt
new file mode 100644
index 0000000..5418616
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import java.util.concurrent.Callable
+
+/**
+ * Logs UI events of Notifications, in particular, logging which Notifications are visible and which
+ * are not.
+ */
+interface NotificationStatsLogger : NotificationRowStatsLogger {
+    fun onLockscreenOrShadeInteractive(
+        isOnLockScreen: Boolean,
+        activeNotifications: List<ActiveNotificationModel>,
+    )
+    fun onLockscreenOrShadeNotInteractive(activeNotifications: List<ActiveNotificationModel>)
+    fun onNotificationRemoved(key: String)
+    fun onNotificationUpdated(key: String)
+    fun onNotificationListUpdated(
+        locationsProvider: Callable<Map<String, Int>>,
+        notificationRanks: Map<String, Int>,
+    )
+    override fun onNotificationExpansionChanged(
+        key: String,
+        isExpanded: Boolean,
+        location: Int,
+        isUserAction: Boolean
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
new file mode 100644
index 0000000..0cb00bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import android.service.notification.NotificationListenerService
+import androidx.annotation.VisibleForTesting
+import com.android.internal.statusbar.IStatusBarService
+import com.android.internal.statusbar.NotificationVisibility
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger
+import com.android.systemui.statusbar.notification.logging.nano.Notifications
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.stack.ExpandableViewState
+import java.util.concurrent.Callable
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@VisibleForTesting const val UNKNOWN_RANK = -1
+
+@SysUISingleton
+class NotificationStatsLoggerImpl
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    private val notificationListenerService: NotificationListenerService,
+    private val notificationPanelLogger: NotificationPanelLogger,
+    private val statusBarService: IStatusBarService,
+) : NotificationStatsLogger {
+    private val lastLoggedVisibilities = mutableMapOf<String, VisibilityState>()
+    private var logVisibilitiesJob: Job? = null
+
+    private val expansionStates: MutableMap<String, ExpansionState> =
+        ConcurrentHashMap<String, ExpansionState>()
+    private val lastReportedExpansionValues: MutableMap<String, Boolean> =
+        ConcurrentHashMap<String, Boolean>()
+
+    override fun onNotificationListUpdated(
+        locationsProvider: Callable<Map<String, Int>>,
+        notificationRanks: Map<String, Int>,
+    ) {
+        if (logVisibilitiesJob?.isActive == true) {
+            return
+        }
+
+        logVisibilitiesJob =
+            startLogVisibilitiesJob(
+                newVisibilities =
+                    combine(
+                        visibilities = locationsProvider.call(),
+                        rankingsMap = notificationRanks
+                    ),
+                activeNotifCount = notificationRanks.size,
+            )
+    }
+
+    override fun onNotificationExpansionChanged(
+        key: String,
+        isExpanded: Boolean,
+        location: Int,
+        isUserAction: Boolean,
+    ) {
+        val expansionState =
+            ExpansionState(
+                key = key,
+                isExpanded = isExpanded,
+                isUserAction = isUserAction,
+                location = location,
+            )
+        expansionStates[key] = expansionState
+        maybeLogNotificationExpansionChange(expansionState)
+    }
+
+    private fun maybeLogNotificationExpansionChange(expansionState: ExpansionState) {
+        if (expansionState.visible.not()) {
+            // Only log visible expansion changes
+            return
+        }
+
+        val loggedExpansionValue: Boolean? = lastReportedExpansionValues[expansionState.key]
+        if (loggedExpansionValue == null && !expansionState.isExpanded) {
+            // Consider the Notification initially collapsed, so only expanded is logged in the
+            // first time.
+            return
+        }
+
+        if (loggedExpansionValue != null && loggedExpansionValue == expansionState.isExpanded) {
+            // We have already logged this state, don't log it again
+            return
+        }
+
+        logNotificationExpansionChange(expansionState)
+        lastReportedExpansionValues[expansionState.key] = expansionState.isExpanded
+    }
+
+    private fun logNotificationExpansionChange(expansionState: ExpansionState) =
+        applicationScope.launch {
+            withContext(bgDispatcher) {
+                statusBarService.onNotificationExpansionChanged(
+                    /* key = */ expansionState.key,
+                    /* userAction = */ expansionState.isUserAction,
+                    /* expanded = */ expansionState.isExpanded,
+                    /* notificationLocation = */ expansionState.location
+                        .toNotificationLocation()
+                        .ordinal
+                )
+            }
+        }
+
+    override fun onLockscreenOrShadeInteractive(
+        isOnLockScreen: Boolean,
+        activeNotifications: List<ActiveNotificationModel>,
+    ) {
+        applicationScope.launch {
+            withContext(bgDispatcher) {
+                notificationPanelLogger.logPanelShown(
+                    isOnLockScreen,
+                    activeNotifications.toNotificationProto()
+                )
+            }
+        }
+    }
+
+    override fun onLockscreenOrShadeNotInteractive(
+        activeNotifications: List<ActiveNotificationModel>
+    ) {
+        logVisibilitiesJob =
+            startLogVisibilitiesJob(
+                newVisibilities = emptyMap(),
+                activeNotifCount = activeNotifications.size
+            )
+    }
+
+    // TODO(b/308623704) wire this in with NotifPipeline updates
+    override fun onNotificationRemoved(key: String) {
+        // No need to track expansion states for Notifications that are removed.
+        expansionStates.remove(key)
+        lastReportedExpansionValues.remove(key)
+    }
+
+    // TODO(b/308623704) wire this in with NotifPipeline updates
+    override fun onNotificationUpdated(key: String) {
+        // When the Notification is updated, we should consider it as not yet logged.
+        lastReportedExpansionValues.remove(key)
+    }
+
+    private fun combine(
+        visibilities: Map<String, Int>,
+        rankingsMap: Map<String, Int>
+    ): Map<String, VisibilityState> =
+        visibilities.mapValues { entry ->
+            VisibilityState(entry.key, entry.value, rankingsMap[entry.key] ?: UNKNOWN_RANK)
+        }
+
+    private fun startLogVisibilitiesJob(
+        newVisibilities: Map<String, VisibilityState>,
+        activeNotifCount: Int,
+    ) =
+        applicationScope.launch {
+            val newlyVisible = newVisibilities - lastLoggedVisibilities.keys
+            val noLongerVisible = lastLoggedVisibilities - newVisibilities.keys
+
+            maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount)
+            updateExpansionStates(newlyVisible, noLongerVisible)
+
+            lastLoggedVisibilities.clear()
+            lastLoggedVisibilities.putAll(newVisibilities)
+        }
+
+    private suspend fun maybeLogVisibilityChanges(
+        newlyVisible: Map<String, VisibilityState>,
+        noLongerVisible: Map<String, VisibilityState>,
+        activeNotifCount: Int,
+    ) {
+        if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
+            return
+        }
+
+        val newlyVisibleAr =
+            newlyVisible.mapToNotificationVisibilitiesAr(visible = true, count = activeNotifCount)
+
+        val noLongerVisibleAr =
+            noLongerVisible.mapToNotificationVisibilitiesAr(
+                visible = false,
+                count = activeNotifCount
+            )
+
+        withContext(bgDispatcher) {
+            statusBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr)
+            if (newlyVisible.isNotEmpty()) {
+                notificationListenerService.setNotificationsShown(newlyVisible.keys.toTypedArray())
+            }
+        }
+    }
+
+    private fun updateExpansionStates(
+        newlyVisible: Map<String, VisibilityState>,
+        noLongerVisible: Map<String, VisibilityState>
+    ) {
+        expansionStates.forEach { (key, expansionState) ->
+            if (newlyVisible.contains(key)) {
+                val newState =
+                    expansionState.copy(
+                        visible = true,
+                        location = newlyVisible.getValue(key).location,
+                    )
+                expansionStates[key] = newState
+                maybeLogNotificationExpansionChange(newState)
+            }
+
+            if (noLongerVisible.contains(key)) {
+                expansionStates[key] =
+                    expansionState.copy(
+                        visible = false,
+                        location = noLongerVisible.getValue(key).location,
+                    )
+            }
+        }
+    }
+
+    private data class VisibilityState(
+        val key: String,
+        val location: Int,
+        val rank: Int,
+    )
+
+    private data class ExpansionState(
+        val key: String,
+        val isUserAction: Boolean,
+        val isExpanded: Boolean,
+        val visible: Boolean,
+        val location: Int,
+    ) {
+        constructor(
+            key: String,
+            isExpanded: Boolean,
+            location: Int,
+            isUserAction: Boolean,
+        ) : this(
+            key = key,
+            isExpanded = isExpanded,
+            isUserAction = isUserAction,
+            visible = isVisibleLocation(location),
+            location = location,
+        )
+    }
+
+    private fun Map<String, VisibilityState>.mapToNotificationVisibilitiesAr(
+        visible: Boolean,
+        count: Int,
+    ): Array<NotificationVisibility> =
+        this.map { (key, state) ->
+                NotificationVisibility.obtain(
+                    /* key = */ key,
+                    /* rank = */ state.rank,
+                    /* count = */ count,
+                    /* visible = */ visible,
+                    /* location = */ state.location.toNotificationLocation()
+                )
+            }
+            .toTypedArray()
+}
+
+private fun Int.toNotificationLocation(): NotificationVisibility.NotificationLocation {
+    return when (this) {
+        ExpandableViewState.LOCATION_FIRST_HUN ->
+            NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP
+        ExpandableViewState.LOCATION_HIDDEN_TOP ->
+            NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP
+        ExpandableViewState.LOCATION_MAIN_AREA ->
+            NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA
+        ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING ->
+            NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING
+        ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN ->
+            NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN
+        ExpandableViewState.LOCATION_GONE ->
+            NotificationVisibility.NotificationLocation.LOCATION_GONE
+        else -> NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN
+    }
+}
+
+private fun List<ActiveNotificationModel>.toNotificationProto(): Notifications.NotificationList {
+    val notificationList = Notifications.NotificationList()
+    val protoArray: Array<Notifications.Notification> =
+        map { notification ->
+                Notifications.Notification().apply {
+                    uid = notification.uid
+                    packageName = notification.packageName
+                    notification.instanceId?.let { instanceId = it }
+                    // TODO(b/308623704) check if we can set groupInstanceId as well
+                    isGroupSummary = notification.isGroupSummary
+                    section = NotificationPanelLogger.toNotificationSection(notification.bucket)
+                }
+            }
+            .toTypedArray()
+
+    if (protoArray.isNotEmpty()) {
+        notificationList.notifications = protoArray
+    }
+
+    return notificationList
+}
+
+private fun isVisibleLocation(location: Int): Boolean =
+    location and ExpandableViewState.VISIBLE_LOCATIONS != 0
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
index 910b40f..c2bc9ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
@@ -16,22 +16,36 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
 import androidx.core.view.doOnDetach
+import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
 
 /**
  * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel].
  */
 object HideNotificationsBinder {
-    suspend fun bindHideList(
+    fun CoroutineScope.bindHideList(
         viewController: NotificationStackScrollLayoutController,
-        viewModel: NotificationListViewModel
+        viewModel: NotificationListViewModel,
+        hiderTracker: DisplaySwitchNotificationsHiderTracker
     ) {
         viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) }
 
-        viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
-            viewController.bindHideState(shouldHide)
+        val hideListFlow = viewModel.hideListViewModel.shouldHideListForPerformance
+            .shareIn(this, started = Lazily)
+
+        launch {
+            hideListFlow.collect { shouldHide ->
+                viewController.bindHideState(shouldHide)
+            }
         }
+
+        launch { hiderTracker.trackNotificationHideTime(hideListFlow) }
+        launch { hiderTracker.trackNotificationHideTimeWhenVisible(hideListFlow) }
     }
 
     private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 1b36660..44a7e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -33,13 +33,17 @@
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
+import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.util.kotlin.getOrNull
+import java.util.Optional
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.combine
@@ -49,13 +53,15 @@
 class NotificationListViewBinder
 @Inject
 constructor(
-    private val viewModel: NotificationListViewModel,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
     private val configuration: ConfigurationState,
     private val falsingManager: FalsingManager,
     private val iconAreaController: NotificationIconAreaController,
     private val metricsLogger: MetricsLogger,
     private val nicBinder: NotificationIconContainerShelfViewBinder,
+    private val loggerOptional: Optional<NotificationStatsLogger>,
+    private val viewModel: NotificationListViewModel,
 ) {
 
     fun bindWhileAttached(
@@ -70,15 +76,20 @@
         view.repeatWhenAttached {
             lifecycleScope.launch {
                 launch { bindShelf(shelf) }
-                launch { bindHideList(viewController, viewModel) }
+                bindHideList(viewController, viewModel, hiderTracker)
 
                 if (FooterViewRefactor.isEnabled) {
                     launch { bindFooter(view) }
                     launch { bindEmptyShade(view) }
-                    viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
-                        view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+                    launch {
+                        viewModel.isImportantForAccessibility.collect { isImportantForAccessibility
+                            ->
+                            view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+                        }
                     }
                 }
+
+                launch { bindLogger(view) }
             }
         }
     }
@@ -136,4 +147,18 @@
                 )
             }
     }
+
+    private suspend fun bindLogger(view: NotificationStackScrollLayout) {
+        if (NotificationsLiveDataStoreRefactor.isEnabled) {
+            viewModel.logger.getOrNull()?.let { viewModel ->
+                loggerOptional.getOrNull()?.let { logger ->
+                    NotificationStatsLoggerBinder.bindLogger(
+                        view,
+                        logger,
+                        viewModel,
+                    )
+                }
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt
new file mode 100644
index 0000000..a05ad6e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLoggerViewModel
+import com.android.systemui.util.kotlin.Utils
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.kotlin.throttle
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+
+/**
+ * Binds a [NotificationStatsLogger] to its [NotificationLoggerViewModel], and wires in
+ * [NotificationStackScrollLayout.OnNotificationLocationsChangedListener] updates to it.
+ */
+object NotificationStatsLoggerBinder {
+
+    /** minimum delay in ms between Notification location updates */
+    private const val NOTIFICATION_UPDATE_PERIOD_MS = 500L
+
+    suspend fun bindLogger(
+        view: NotificationStackScrollLayout,
+        logger: NotificationStatsLogger,
+        viewModel: NotificationLoggerViewModel,
+    ) {
+        viewModel.isLockscreenOrShadeInteractive
+            .sample(
+                combine(viewModel.isOnLockScreen, viewModel.activeNotifications, ::Pair),
+                Utils.Companion::toTriple
+            )
+            .collectLatest { (isPanelInteractive, isOnLockScreen, notifications) ->
+                if (isPanelInteractive) {
+                    logger.onLockscreenOrShadeInteractive(
+                        isOnLockScreen = isOnLockScreen,
+                        activeNotifications = notifications,
+                    )
+                    view.onNotificationsUpdated
+                        // Delay the updates with [NOTIFICATION_UPDATES_PERIOD_MS]. If the original
+                        // flow emits more than once during this period, only the latest value is
+                        // emitted, meaning that we won't log the intermediate Notification states.
+                        .throttle(NOTIFICATION_UPDATE_PERIOD_MS)
+                        .sample(viewModel.activeNotificationRanks, ::Pair)
+                        .collect { (locationsProvider, notificationRanks) ->
+                            logger.onNotificationListUpdated(locationsProvider, notificationRanks)
+                        }
+                } else {
+                    logger.onLockscreenOrShadeNotInteractive(
+                        activeNotifications = notifications,
+                    )
+                }
+            }
+    }
+}
+
+private val NotificationStackScrollLayout.onNotificationsUpdated
+    get() =
+        ConflatedCallbackFlow.conflatedCallbackFlow {
+            val callback =
+                NotificationStackScrollLayout.OnNotificationLocationsChangedListener { callable ->
+                    trySend(callable)
+                }
+            setNotificationLocationsChangedListener(callback)
+            awaitClose { setNotificationLocationsChangedListener(null) }
+        }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 569ae24..86c0a678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -40,6 +40,7 @@
     val shelf: NotificationShelfViewModel,
     val hideListViewModel: HideListViewModel,
     val footer: Optional<FooterViewModel>,
+    val logger: Optional<NotificationLoggerViewModel>,
     activeNotificationsInteractor: ActiveNotificationsInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     seenNotificationsInteractor: SeenNotificationsInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt
new file mode 100644
index 0000000..0901a7f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class NotificationLoggerViewModel
+@Inject
+constructor(
+    activeNotificationsInteractor: ActiveNotificationsInteractor,
+    keyguardInteractor: KeyguardInteractor,
+    windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor,
+) {
+    val activeNotifications: Flow<List<ActiveNotificationModel>> =
+        activeNotificationsInteractor.allRepresentativeNotifications.map { it.values.toList() }
+
+    val activeNotificationRanks: Flow<Map<String, Int>> =
+        activeNotificationsInteractor.activeNotificationRanks
+
+    val isLockscreenOrShadeInteractive: Flow<Boolean> =
+        windowRootViewVisibilityInteractor.isLockscreenOrShadeVisibleAndInteractive
+
+    val isOnLockScreen: Flow<Boolean> = keyguardInteractor.isKeyguardShowing
+}
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 5ee38be..a48fb45 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
@@ -240,13 +240,13 @@
      */
     val translationY: Flow<Float> =
         combine(
-            isOnLockscreen,
+            isOnLockscreenWithoutShade,
             merge(
                 keyguardInteractor.keyguardTranslationY,
                 occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
             )
-        ) { isOnLockscreen, translationY ->
-            if (isOnLockscreen) {
+        ) { isOnLockscreenWithoutShade, translationY ->
+            if (isOnLockscreenWithoutShade) {
                 translationY
             } else {
                 0f
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt
new file mode 100644
index 0000000..252945f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.dagger
+
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import javax.inject.Qualifier
+
+/** Detailed [DeviceBasedSatelliteRepository] logs */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class OemSatelliteInputLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index e309c32..2b90e64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -265,6 +265,13 @@
             return factory.create("VerboseMobileViewLog", 100)
         }
 
+        @Provides
+        @SysUISingleton
+        @OemSatelliteInputLog
+        fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer {
+            return factory.create("DeviceBasedSatelliteInputLog", 32)
+        }
+
         const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
             "FirstMobileSubShowingNetworkTypeIcon"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
index e3c3139..8400fb0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.satellite.ui.DeviceBasedSatelliteBindableIcon
 import javax.inject.Inject
 
 /**
@@ -38,11 +39,12 @@
 class BindableIconsRegistryImpl
 @Inject
 constructor(
-/** Bindables go here */
+    /** Bindables go here */
+    oemSatellite: DeviceBasedSatelliteBindableIcon
 ) : BindableIconsRegistry {
     /**
      * Adding the injected bindables to this list will get them registered with
      * StatusBarIconController
      */
-    override val bindableIcons: List<BindableIcon> = listOf()
+    override val bindableIcons: List<BindableIcon> = listOf(oemSatellite)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index de46a5e..5e6e36d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -19,11 +19,17 @@
 import android.os.OutcomeReceiver
 import android.telephony.satellite.NtnSignalStrengthCallback
 import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
 import android.telephony.satellite.SatelliteModemStateCallback
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.MessageInitializer
+import com.android.systemui.log.core.MessagePrinter
+import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
@@ -123,6 +129,7 @@
     satelliteManagerOpt: Optional<SatelliteManager>,
     @Background private val bgDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
+    @OemSatelliteInputLog private val logBuffer: LogBuffer,
     private val systemClock: SystemClock,
 ) : DeviceBasedSatelliteRepository {
 
@@ -145,6 +152,11 @@
                 ensureMinUptime(systemClock, MIN_UPTIME)
                 satelliteSupport.value = satelliteManager.checkSatelliteSupported()
 
+                logBuffer.i(
+                    { str1 = satelliteSupport.value.toString() },
+                    { "Checked for system support. support=$str1" },
+                )
+
                 // We only need to check location availability if this mode is supported
                 if (satelliteSupport.value is Supported) {
                     isSatelliteAllowedForCurrentLocation.subscriptionCount
@@ -159,6 +171,9 @@
                                  * connection might cause more frequent checks.
                                  */
                                 while (true) {
+                                    logBuffer.i {
+                                        "requestIsSatelliteCommunicationAllowedForCurrentLocation"
+                                    }
                                     checkIsSatelliteAllowed()
                                     delay(POLLING_INTERVAL_MS)
                                 }
@@ -167,6 +182,8 @@
                 }
             }
         } else {
+            logBuffer.i { "Satellite manager is null" }
+
             satelliteSupport.value = NotSupported
         }
     }
@@ -181,12 +198,21 @@
     private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
         conflatedCallbackFlow {
                 val cb = SatelliteModemStateCallback { state ->
+                    logBuffer.i({ int1 = state }) { "onSatelliteModemStateChanged: state=$int1" }
                     trySend(SatelliteConnectionState.fromModemState(state))
                 }
 
-                sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+                var registered = false
 
-                awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) }
+                try {
+                    val res =
+                        sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+                    registered = res == SATELLITE_RESULT_SUCCESS
+                } catch (e: Exception) {
+                    logBuffer.e("error registering for modem state", e)
+                }
+
+                awaitClose { if (registered) sm.unregisterForSatelliteModemStateChanged(cb) }
             }
             .flowOn(bgDispatcher)
 
@@ -197,12 +223,21 @@
     private fun signalStrengthFlow(sm: SupportedSatelliteManager) =
         conflatedCallbackFlow {
                 val cb = NtnSignalStrengthCallback { signalStrength ->
+                    logBuffer.i({ int1 = signalStrength.level }) {
+                        "onNtnSignalStrengthChanged: level=$int1"
+                    }
                     trySend(signalStrength.level)
                 }
 
-                sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+                var registered = false
+                try {
+                    sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+                    registered = true
+                } catch (e: Exception) {
+                    logBuffer.e("error registering for signal strength", e)
+                }
 
-                awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) }
+                awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) }
             }
             .flowOn(bgDispatcher)
 
@@ -213,11 +248,15 @@
                 bgDispatcher.asExecutor(),
                 object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
                     override fun onError(e: SatelliteManager.SatelliteException) {
-                        android.util.Log.e(TAG, "Found exception when checking for satellite: ", e)
+                        logBuffer.e(
+                            "Found exception when checking availability",
+                            e,
+                        )
                         isSatelliteAllowedForCurrentLocation.value = false
                     }
 
                     override fun onResult(allowed: Boolean) {
+                        logBuffer.i { allowed.toString() }
                         isSatelliteAllowedForCurrentLocation.value = allowed
                     }
                 }
@@ -239,6 +278,12 @@
                     }
 
                     override fun onError(error: SatelliteManager.SatelliteException) {
+                        logBuffer.e(
+                            "Exception when checking for satellite support. " +
+                                "Assuming it is not supported for this device.",
+                            error,
+                        )
+
                         // Assume that an error means it's not supported
                         continuation.resume(NotSupported)
                     }
@@ -264,5 +309,19 @@
                 delay(timeTilMinUptime)
             }
         }
+
+        /** A couple of convenience logging methods rather than a whole class */
+        private fun LogBuffer.i(
+            initializer: MessageInitializer = {},
+            printer: MessagePrinter,
+        ) = this.log(TAG, LogLevel.INFO, initializer, printer)
+
+        private fun LogBuffer.e(message: String, exception: Throwable? = null) =
+            this.log(
+                tag = TAG,
+                level = LogLevel.ERROR,
+                message = message,
+                exception = exception,
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt
new file mode 100644
index 0000000..f5d0f6b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui
+
+import android.content.Context
+import com.android.internal.telephony.flags.Flags.oemEnabledSatelliteFlag
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
+import com.android.systemui.statusbar.pipeline.satellite.ui.binder.DeviceBasedSatelliteIconBinder
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+import javax.inject.Inject
+
+@SysUISingleton
+class DeviceBasedSatelliteBindableIcon
+@Inject
+constructor(
+    context: Context,
+    viewModel: DeviceBasedSatelliteViewModel,
+) : BindableIcon {
+    override val slot: String =
+        context.getString(com.android.internal.R.string.status_bar_oem_satellite)
+
+    override val initializer = ModernStatusBarViewCreator { context ->
+        SingleBindableStatusBarIconView.createView(context).also { view ->
+            view.initView(slot) { DeviceBasedSatelliteIconBinder.bind(view, viewModel) }
+        }
+    }
+
+    override val shouldBindIcon: Boolean = oemEnabledSatelliteFlag()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt
new file mode 100644
index 0000000..59ac5f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.binder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+import kotlinx.coroutines.launch
+
+object DeviceBasedSatelliteIconBinder {
+    fun bind(
+        view: SingleBindableStatusBarIconView,
+        viewModel: DeviceBasedSatelliteViewModel,
+    ): ModernStatusBarViewBinding {
+        return SingleBindableStatusBarIconView.withDefaultBinding(
+            view = view,
+            shouldBeVisible = { viewModel.icon.value != null }
+        ) {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.icon.collect { newIcon ->
+                        if (newIcon == null) {
+                            view.iconView.setImageDrawable(null)
+                        } else {
+                            IconViewBinder.bind(newIcon, view.iconView)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
new file mode 100644
index 0000000..6938d66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.model
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+
+/**
+ * Define the [Icon] that relates to a given satellite connection state + level. Note that for now
+ * We don't need any data class box, so we can just use a simple mapping function.
+ */
+object SatelliteIconModel {
+    fun fromConnectionState(
+        connectionState: SatelliteConnectionState,
+        signalStrength: Int,
+    ): Icon? =
+        when (connectionState) {
+            // TODO(b/316635648): check if this should be null
+            SatelliteConnectionState.Unknown,
+            SatelliteConnectionState.Off,
+            SatelliteConnectionState.On ->
+                Icon.Resource(
+                    res = R.drawable.ic_satellite_not_connected,
+                    contentDescription = null,
+                )
+            SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength)
+        }
+
+    private fun fromSignalStrength(
+        signalStrength: Int,
+    ): Icon? =
+        // TODO(b/316634365): these need content descriptions
+        when (signalStrength) {
+            // No signal
+            0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null)
+
+            // Poor -> Moderate
+            1,
+            2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null)
+
+            // Good -> Great
+            3,
+            4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null)
+            else -> null
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
new file mode 100644
index 0000000..0051161
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * View-Model for the device-based satellite icon. This icon will only show in the status bar if
+ * satellite is available AND all other service states are considered OOS.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class DeviceBasedSatelliteViewModel
+@Inject
+constructor(
+    interactor: DeviceBasedSatelliteInteractor,
+    @Application scope: CoroutineScope,
+) {
+    private val shouldShowIcon: StateFlow<Boolean> =
+        interactor.areAllConnectionsOutOfService
+            .flatMapLatest { allOos ->
+                if (!allOos) {
+                    flowOf(false)
+                } else {
+                    interactor.isSatelliteAllowed
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    val icon: StateFlow<Icon?> =
+        combine(
+                shouldShowIcon,
+                interactor.connectionState,
+                interactor.signalStrength,
+            ) { shouldShow, state, signalStrength ->
+                if (shouldShow) {
+                    SatelliteIconModel.fromConnectionState(state, signalStrength)
+                } else {
+                    null
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
index 3b87bed..25a2c9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
@@ -103,7 +103,7 @@
      *
      * Creates a dot view, and uses [bindingCreator] to get and set the binding.
      */
-    fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
+    open fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
         // The dot view requires [slot] to be set, and the [binding] may require an instantiated dot
         // view. So, this is the required order.
         this.slot = slot
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt
new file mode 100644
index 0000000..c663c37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.view
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+/** Simple single-icon view that is bound to bindable_status_bar_icon.xml */
+class SingleBindableStatusBarIconView(
+    context: Context,
+    attrs: AttributeSet?,
+) : ModernStatusBarView(context, attrs) {
+
+    internal lateinit var iconView: ImageView
+    internal lateinit var dotView: StatusBarIconView
+
+    override fun toString(): String {
+        return "SingleBindableStatusBarIcon(" +
+            "slot='$slot', " +
+            "isCollecting=${binding.isCollecting()}, " +
+            "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " +
+            "viewString=${super.toString()}"
+    }
+
+    override fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
+        super.initView(slot, bindingCreator)
+
+        iconView = requireViewById(R.id.icon_view)
+        dotView = requireViewById(R.id.status_bar_dot)
+    }
+
+    companion object {
+        fun createView(
+            context: Context,
+        ): SingleBindableStatusBarIconView {
+            return LayoutInflater.from(context).inflate(R.layout.bindable_status_bar_icon, null)
+                as SingleBindableStatusBarIconView
+        }
+
+        /**
+         * Using a given binding [block], create the necessary scaffolding to handle the general
+         * case of a single status bar icon. This includes eliding into a dot view when there is not
+         * enough space, and handling tint.
+         *
+         * [block] should be a simple [launch] call that handles updating the single icon view with
+         * its new view. Currently there is no simple way to e.g., extend to handle multiple tints
+         * for dual-layered icons, and any more complex logic should probably find a way to return
+         * its own version of [ModernStatusBarViewBinding].
+         */
+        fun withDefaultBinding(
+            view: SingleBindableStatusBarIconView,
+            shouldBeVisible: () -> Boolean,
+            block: suspend LifecycleOwner.(View) -> Unit
+        ): SingleBindableStatusBarIconViewBinding {
+            @StatusBarIconView.VisibleState
+            val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
+
+            val iconTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE)
+            val decorTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE)
+
+            var isCollecting: Boolean = false
+
+            view.repeatWhenAttached {
+                // Child binding
+                block(view)
+
+                lifecycleScope.launch {
+                    repeatOnLifecycle(Lifecycle.State.STARTED) {
+                        // isVisible controls the visibility state of the outer group, and thus it
+                        // needs
+                        // to run in the CREATED lifecycle so it can continue to watch while
+                        // invisible
+                        // See (b/291031862) for details
+                        launch {
+                            visibilityState.collect { visibilityState ->
+                                // for b/296864006, we can not hide all the child views if
+                                // visibilityState is STATE_HIDDEN. Because hiding all child views
+                                // would cause the
+                                // getWidth() of this view return 0, and that would cause the
+                                // translation
+                                // calculation fails in StatusIconContainer. Therefore, like class
+                                // MobileIconBinder, instead of set the child views visibility to
+                                // View.GONE,
+                                // we set their visibility to View.INVISIBLE to make them invisible
+                                // but
+                                // keep the width.
+                                ModernStatusBarViewVisibilityHelper.setVisibilityState(
+                                    visibilityState,
+                                    view.iconView,
+                                    view.dotView,
+                                )
+                            }
+                        }
+
+                        launch {
+                            iconTint.collect { tint ->
+                                val tintList = ColorStateList.valueOf(tint)
+                                view.iconView.imageTintList = tintList
+                                view.dotView.setDecorColor(tint)
+                            }
+                        }
+
+                        launch {
+                            decorTint.collect { decorTint -> view.dotView.setDecorColor(decorTint) }
+                        }
+
+                        try {
+                            awaitCancellation()
+                        } finally {
+                            isCollecting = false
+                        }
+                    }
+                }
+            }
+
+            return object : SingleBindableStatusBarIconViewBinding {
+                override val decorTint: Int
+                    get() = decorTint.value
+
+                override val iconTint: Int
+                    get() = iconTint.value
+
+                override fun getShouldIconBeVisible(): Boolean {
+                    return shouldBeVisible()
+                }
+
+                override fun onVisibilityStateChanged(state: Int) {
+                    visibilityState.value = state
+                }
+
+                override fun onIconTintChanged(newTint: Int, contrastTint: Int) {
+                    iconTint.value = newTint
+                }
+
+                override fun onDecorTintChanged(newTint: Int) {
+                    decorTint.value = newTint
+                }
+
+                override fun isCollecting(): Boolean {
+                    return isCollecting
+                }
+            }
+        }
+    }
+}
+
+@VisibleForTesting
+interface SingleBindableStatusBarIconViewBinding : ModernStatusBarViewBinding {
+    val iconTint: Int
+    val decorTint: Int
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index 3d7d701..d8ef981 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -175,7 +175,7 @@
                 mPrimaryBouncerInteractor,
                 mContext,
                 () -> mDeviceEntryInteractor,
-                mSceneTestUtils.getSceneContainerFlags()
+                mSceneTestUtils.getFakeSceneContainerFlags()
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
index 93a5393..132bdb5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
@@ -373,7 +373,7 @@
     @Test
     public void longPress_showBouncer_sceneContainerNotEnabled() {
         init(/* useMigrationFlag= */ false);
-        mSceneTestUtils.getSceneContainerFlags().setEnabled(false);
+        mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(false);
         when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(false);
 
         // WHEN longPress
@@ -387,7 +387,7 @@
     @Test
     public void longPress_showBouncer() {
         init(/* useMigrationFlag= */ false);
-        mSceneTestUtils.getSceneContainerFlags().setEnabled(true);
+        mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(true);
         when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(false);
 
         // WHEN longPress
@@ -401,7 +401,7 @@
     @Test
     public void longPress_falsingTriggered_doesNotShowBouncer() {
         init(/* useMigrationFlag= */ false);
-        mSceneTestUtils.getSceneContainerFlags().setEnabled(true);
+        mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(true);
         when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(true);
 
         // WHEN longPress
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
index 647dae6..13306be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
@@ -20,6 +20,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -44,6 +45,7 @@
     @Mock lateinit var shadeInteractor: ShadeInteractor
     @Mock lateinit var systemUIDialogManager: SystemUIDialogManager
     @Mock lateinit var dumpManager: DumpManager
+    @Mock lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor
 
     private lateinit var udfpsBpViewController: UdfpsBpViewController
 
@@ -55,7 +57,8 @@
                 statusBarStateController,
                 shadeInteractor,
                 systemUIDialogManager,
-                dumpManager
+                dumpManager,
+                udfpsOverlayInteractor,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
index 8f8004f..b1e471a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
@@ -5,6 +5,7 @@
 import android.hardware.biometrics.IBiometricContextListener.FoldState
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthController
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
@@ -17,6 +18,7 @@
 import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
 import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
 import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -42,7 +44,10 @@
     private val testScope = TestScope()
 
     @Mock private lateinit var foldProvider: FoldStateProvider
+    @Mock private lateinit var authController: AuthController
+    @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
 
+    private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor
     private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
 
     private lateinit var interactor: LogContextInteractorImpl
@@ -50,6 +55,13 @@
     @Before
     fun setup() {
         keyguardTransitionRepository = FakeKeyguardTransitionRepository()
+        udfpsOverlayInteractor =
+            UdfpsOverlayInteractor(
+                context,
+                authController,
+                selectedUserInteractor,
+                testScope.backgroundScope,
+            )
         interactor =
             LogContextInteractorImpl(
                 testScope.backgroundScope,
@@ -59,6 +71,7 @@
                         scope = testScope.backgroundScope,
                     )
                     .keyguardTransitionInteractor,
+                udfpsOverlayInteractor,
             )
     }
 
@@ -162,6 +175,18 @@
         }
 
     @Test
+    fun isHardwareIgnoringTouchesChanges() =
+        testScope.runTest {
+            val isHardwareIgnoringTouches by collectLastValue(interactor.isHardwareIgnoringTouches)
+
+            udfpsOverlayInteractor.setHandleTouches(true)
+            assertThat(isHardwareIgnoringTouches).isFalse()
+
+            udfpsOverlayInteractor.setHandleTouches(false)
+            assertThat(isHardwareIgnoringTouches).isTrue()
+        }
+
+    @Test
     fun foldStateChanges() =
         testScope.runTest {
             val foldState = collectLastValue(interactor.foldState)
@@ -195,6 +220,7 @@
 
             var folded: Int? = null
             var displayState: Int? = null
+            var ignoreTouches: Boolean? = null
             val job =
                 interactor.addBiometricContextListener(
                     object : IBiometricContextListener.Stub() {
@@ -205,12 +231,17 @@
                         override fun onDisplayStateChanged(newDisplayState: Int) {
                             displayState = newDisplayState
                         }
+
+                        override fun onHardwareIgnoreTouchesChanged(newIgnoreTouches: Boolean) {
+                            ignoreTouches = newIgnoreTouches
+                        }
                     }
                 )
             runCurrent()
 
             assertThat(folded).isEqualTo(FoldState.FULLY_CLOSED)
             assertThat(displayState).isEqualTo(AuthenticateOptions.DISPLAY_STATE_AOD)
+            assertThat(ignoreTouches).isFalse()
 
             foldListener.onFoldUpdate(FOLD_UPDATE_START_OPENING)
             foldListener.onFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
@@ -220,6 +251,11 @@
             assertThat(folded).isEqualTo(FoldState.HALF_OPENED)
             assertThat(displayState).isEqualTo(AuthenticateOptions.DISPLAY_STATE_LOCKSCREEN)
 
+            udfpsOverlayInteractor.setHandleTouches(false)
+            runCurrent()
+
+            assertThat(ignoreTouches).isTrue()
+
             job.cancel()
 
             // stale updates should be ignored
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
index 6a68672..c0e108e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt
@@ -68,7 +68,7 @@
     @Test
     fun testShouldInterceptTouch() =
         testScope.runTest {
-            createUdpfsOverlayInteractor()
+            createUdfpsOverlayInteractor()
 
             // When fingerprint enrolled and touch is within bounds
             verify(authController).addCallback(authControllerCallback.capture())
@@ -92,7 +92,7 @@
     @Test
     fun testUdfpsOverlayParamsChange() =
         testScope.runTest {
-            createUdpfsOverlayInteractor()
+            createUdfpsOverlayInteractor()
             val udfpsOverlayParams = collectLastValue(underTest.udfpsOverlayParams)
             runCurrent()
 
@@ -105,7 +105,7 @@
             assertThat(udfpsOverlayParams()).isEqualTo(firstParams)
         }
 
-    private fun createUdpfsOverlayInteractor() {
+    private fun createUdfpsOverlayInteractor() {
         underTest =
             UdfpsOverlayInteractor(
                 context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
index 9e3c576..bd4973d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -1,5 +1,7 @@
 package com.android.systemui.biometrics.domain.model
 
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptVerticalListContentView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
@@ -23,11 +25,21 @@
         val title = "what"
         val subtitle = "a"
         val description = "request"
+        val contentView =
+            PromptVerticalListContentView.Builder()
+                .setDescription("content description")
+                .addListItem(PromptContentListItemBulletedText("content text"))
+                .build()
 
         val fpPros = fingerprintSensorPropertiesInternal().first()
         val request =
             BiometricPromptRequest.Biometric(
-                promptInfo(title = title, subtitle = subtitle, description = description),
+                promptInfo(
+                    title = title,
+                    subtitle = subtitle,
+                    description = description,
+                    contentView = contentView
+                ),
                 BiometricUserInfo(USER_ID),
                 BiometricOperationInfo(OPERATION_ID),
                 BiometricModalities(fingerprintProperties = fpPros),
@@ -36,6 +48,7 @@
         assertThat(request.title).isEqualTo(title)
         assertThat(request.subtitle).isEqualTo(subtitle)
         assertThat(request.description).isEqualTo(description)
+        assertThat(request.contentView).isEqualTo(contentView)
         assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
         assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
         assertThat(request.modalities)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 6170e0c..bf61c2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -18,7 +18,10 @@
 
 import android.content.res.Configuration
 import android.graphics.Point
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptContentView
 import android.hardware.biometrics.PromptInfo
+import android.hardware.biometrics.PromptVerticalListContentView
 import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
@@ -60,7 +63,6 @@
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -69,7 +71,6 @@
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 import org.mockito.Mock
-import org.mockito.Mockito.times
 import org.mockito.junit.MockitoJUnit
 
 private const val USER_ID = 4
@@ -101,6 +102,7 @@
     private lateinit var selector: PromptSelectorInteractor
     private lateinit var viewModel: PromptViewModel
     private lateinit var iconViewModel: PromptIconViewModel
+    private lateinit var promptContentView: PromptContentView
 
     @Before
     fun setup() {
@@ -136,6 +138,10 @@
         selector =
             PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils)
         selector.resetPrompt()
+        promptContentView =
+            PromptVerticalListContentView.Builder()
+                .addListItem(PromptContentListItemBulletedText("test"))
+                .build()
 
         viewModel =
             PromptViewModel(
@@ -1200,6 +1206,26 @@
         }
     }
 
+    @Test
+    fun descriptionOverriddenByContentView() =
+        runGenericTest(contentView = promptContentView, description = "test description") {
+            val contentView by collectLastValue(viewModel.contentView)
+            val description by collectLastValue(viewModel.description)
+
+            assertThat(description).isEqualTo("")
+            assertThat(contentView).isEqualTo(promptContentView)
+        }
+
+    @Test
+    fun descriptionWithoutContentView() =
+        runGenericTest(description = "test description") {
+            val contentView by collectLastValue(viewModel.contentView)
+            val description by collectLastValue(viewModel.description)
+
+            assertThat(description).isEqualTo("test description")
+            assertThat(contentView).isNull()
+        }
+
     /** Asserts that the selected buttons are visible now. */
     private suspend fun TestScope.assertButtonsVisible(
         tryAgain: Boolean = false,
@@ -1219,6 +1245,8 @@
     private fun runGenericTest(
         doNotStart: Boolean = false,
         allowCredentialFallback: Boolean = false,
+        description: String? = null,
+        contentView: PromptContentView? = null,
         block: suspend TestScope.() -> Unit
     ) {
         selector.initializePrompt(
@@ -1226,6 +1254,8 @@
             allowCredentialFallback = allowCredentialFallback,
             fingerprint = testCase.fingerprint,
             face = testCase.face,
+            descriptionFromApp = description,
+            contentViewFromApp = contentView,
         )
 
         // put the view model in the initial authenticating state, unless explicitly skipped
@@ -1401,11 +1431,15 @@
     face: FaceSensorPropertiesInternal? = null,
     requireConfirmation: Boolean = false,
     allowCredentialFallback: Boolean = false,
+    descriptionFromApp: String? = null,
+    contentViewFromApp: PromptContentView? = null,
 ) {
     val info =
         PromptInfo().apply {
             title = "t"
             subtitle = "s"
+            description = descriptionFromApp
+            contentView = contentViewFromApp
             authenticators = listOf(face, fingerprint).extractAuthenticatorTypes()
             isDeviceCredentialAllowed = allowCredentialFallback
             isConfirmationRequested = requireConfirmation
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt
index d5c3641..0dfdeca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt
@@ -36,9 +36,9 @@
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.testKosmos
-import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -72,8 +72,8 @@
 
             // It's been 10 seconds since the last power button wakeup
             setAwakeFromPowerButton()
+            advanceTimeBy(10000)
             runCurrent()
-            kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 10000)
 
             enterDeviceFromBiometricUnlock()
             assertThat(playSuccessHaptic).isNotNull()
@@ -89,8 +89,8 @@
 
             // It's been 10 seconds since the last power button wakeup
             setAwakeFromPowerButton()
+            advanceTimeBy(10000)
             runCurrent()
-            kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 10000)
 
             enterDeviceFromBiometricUnlock()
             assertThat(playSuccessHaptic).isNull()
@@ -106,8 +106,8 @@
 
             // It's only been 50ms since the last power button wakeup
             setAwakeFromPowerButton()
+            advanceTimeBy(50)
             runCurrent()
-            kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 50)
 
             enterDeviceFromBiometricUnlock()
             assertThat(playSuccessHaptic).isNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index e531d44..0ea4e9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -20,8 +20,13 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
@@ -50,6 +55,8 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
@@ -102,9 +109,12 @@
         FromPrimaryBouncerTransitionInteractor
     private lateinit var fromDreamingLockscreenHostedTransitionInteractor:
         FromDreamingLockscreenHostedTransitionInteractor
+    private lateinit var fromGlanceableHubTransitionInteractor:
+        FromGlanceableHubTransitionInteractor
 
     private lateinit var powerInteractor: PowerInteractor
     private lateinit var keyguardInteractor: KeyguardInteractor
+    private lateinit var communalInteractor: CommunalInteractor
 
     @Before
     fun setUp() {
@@ -118,10 +128,13 @@
         shadeRepository = FakeShadeRepository()
         transitionRepository = spy(FakeKeyguardTransitionRepository())
         powerInteractor = PowerInteractorFactory.create().powerInteractor
+        communalInteractor =
+            CommunalInteractorFactory.create(testScope = testScope).communalInteractor
 
         whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
 
         featureFlags = FakeFeatureFlags().apply { set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) }
+        mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
 
         keyguardInteractor = createKeyguardInteractor()
 
@@ -137,6 +150,13 @@
                 )
                 .keyguardTransitionInteractor
 
+        val glanceableHubTransitions =
+            GlanceableHubTransitions(
+                testScope,
+                transitionInteractor,
+                transitionRepository,
+                communalInteractor
+            )
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                     scope = testScope,
@@ -148,6 +168,7 @@
                     flags = featureFlags,
                     shadeRepository = shadeRepository,
                     powerInteractor = powerInteractor,
+                    glanceableHubTransitions = glanceableHubTransitions,
                     inWindowLauncherUnlockAnimationInteractor = {
                         InWindowLauncherUnlockAnimationInteractor(
                             InWindowLauncherUnlockAnimationRepository(),
@@ -255,6 +276,16 @@
                     powerInteractor = powerInteractor,
                 )
                 .apply { start() }
+
+        fromGlanceableHubTransitionInteractor =
+            FromGlanceableHubTransitionInteractor(
+                    bgDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
+                    glanceableHubTransitions = glanceableHubTransitions,
+                    transitionRepository = transitionRepository,
+                    transitionInteractor = transitionInteractor,
+                )
+                .apply { start() }
     }
 
     @Test
@@ -1410,6 +1441,124 @@
             coroutineContext.cancelChildren()
         }
 
+    @Test
+    fun lockscreenToGlanceableHub() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to LOCKSCREEN
+            runTransitionAndSetWakefulness(KeyguardState.AOD, KeyguardState.LOCKSCREEN)
+            runCurrent()
+
+            // WHEN a glanceable hub transition starts
+            val currentScene = CommunalSceneKey.Blank
+            val targetScene = CommunalSceneKey.Communal
+
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            communalInteractor.setTransitionState(transitionState)
+            progress.value = .1f
+            runCurrent()
+
+            // THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            assertThat(info.ownerName)
+                .isEqualTo(FromLockscreenTransitionInteractor::class.simpleName)
+            assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info.animator).isNull() // transition should be manually animated
+
+            // WHEN the user stops dragging and the glanceable hub opening is cancelled
+            clearInvocations(transitionRepository)
+            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+            val idleTransitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            communalInteractor.setTransitionState(idleTransitionState)
+            runCurrent()
+
+            // THEN a transition from GLANCEABLE_HUB => LOCKSCREEN should occur
+            val info2 =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            assertThat(info2.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info2.to).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.animator).isNull() // transition should be manually animated
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun glanceableHubToLockscreen() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to GLANCEABLE_HUB
+            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+            runCurrent()
+
+            // WHEN a transition away from glanceable hub starts
+            val currentScene = CommunalSceneKey.Communal
+            val targetScene = CommunalSceneKey.Blank
+
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Transition(
+                        fromScene = currentScene,
+                        toScene = targetScene,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            communalInteractor.setTransitionState(transitionState)
+            progress.value = .1f
+            runCurrent()
+
+            // THEN a transition from GLANCEABLE_HUB => LOCKSCREEN should occur
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            assertThat(info.ownerName)
+                .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+            assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.animator).isNull() // transition should be manually animated
+
+            // WHEN the user stops dragging and the glanceable hub closing is cancelled
+            clearInvocations(transitionRepository)
+            runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN)
+            val idleTransitionState =
+                MutableStateFlow<ObservableCommunalTransitionState>(
+                    ObservableCommunalTransitionState.Idle(currentScene)
+                )
+            communalInteractor.setTransitionState(idleTransitionState)
+            runCurrent()
+
+            // THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur
+            val info2 =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            assertThat(info2.from).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info2.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info.animator).isNull() // transition should be manually animated
+
+            coroutineContext.cancelChildren()
+        }
+
     private fun createKeyguardInteractor(): KeyguardInteractor {
         return KeyguardInteractorFactory.create(
                 featureFlags = featureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
index a4d217f..5dd37ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
@@ -21,13 +21,17 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardClockSwitch.LARGE
+import com.android.keyguard.KeyguardClockSwitch.SMALL
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.plugins.clocks.ClockConfig
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.plugins.clocks.ClockFaceLayout
 import com.android.systemui.util.mockito.whenever
 import kotlin.test.Test
+import kotlinx.coroutines.flow.MutableStateFlow
 import org.junit.Before
 import org.junit.runner.RunWith
 import org.mockito.Mock
@@ -48,32 +52,60 @@
     @Mock private lateinit var smallClockView: View
     @Mock private lateinit var smallClockFaceLayout: ClockFaceLayout
     @Mock private lateinit var largeClockFaceLayout: ClockFaceLayout
+    @Mock private lateinit var clockViewModel: KeyguardClockViewModel
+    private val clockSize = MutableStateFlow(LARGE)
+    private val currentClock: MutableStateFlow<ClockController?> = MutableStateFlow(null)
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-    }
-
-    @Test
-    fun addClockViews_nonWeatherClock() {
-        setupNonWeatherClock()
-        KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer)
-        verify(rootView).addView(smallClockView)
-        verify(rootView).addView(largeClockView)
-        verify(burnInLayer).addView(smallClockView)
-        verify(burnInLayer, never()).addView(largeClockView)
+        whenever(clockViewModel.clockSize).thenReturn(clockSize)
+        whenever(clockViewModel.currentClock).thenReturn(currentClock)
+        whenever(clockViewModel.burnInLayer).thenReturn(burnInLayer)
     }
 
     @Test
     fun addClockViews_WeatherClock() {
         setupWeatherClock()
-        KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer)
+        KeyguardClockViewBinder.addClockViews(clock, rootView)
         verify(rootView).addView(smallClockView)
         verify(rootView).addView(largeClockView)
-        verify(burnInLayer).addView(smallClockView)
+    }
+
+    @Test
+    fun addClockViews_nonWeatherClock() {
+        setupNonWeatherClock()
+        KeyguardClockViewBinder.addClockViews(clock, rootView)
+        verify(rootView).addView(smallClockView)
+        verify(rootView).addView(largeClockView)
+    }
+    @Test
+    fun addClockViewsToBurnInLayer_LargeWeatherClock() {
+        setupWeatherClock()
+        clockSize.value = LARGE
+        KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel)
+        verify(burnInLayer).removeView(smallClockView)
         verify(burnInLayer).addView(largeClockView)
     }
 
+    @Test
+    fun addClockViewsToBurnInLayer_LargeNonWeatherClock() {
+        setupNonWeatherClock()
+        clockSize.value = LARGE
+        KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel)
+        verify(burnInLayer).removeView(smallClockView)
+        verify(burnInLayer, never()).addView(largeClockView)
+    }
+
+    @Test
+    fun addClockViewsToBurnInLayer_SmallClock() {
+        setupNonWeatherClock()
+        clockSize.value = SMALL
+        KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel)
+        verify(burnInLayer).addView(smallClockView)
+        verify(burnInLayer).removeView(largeClockView)
+    }
+
     private fun setupWeatherClock() {
         setupClock()
         val clockConfig =
@@ -99,5 +131,7 @@
         whenever(clock.smallClock).thenReturn(smallClock)
         whenever(largeClock.layout).thenReturn(largeClockFaceLayout)
         whenever(smallClock.layout).thenReturn(smallClockFaceLayout)
+        whenever(clockViewModel.clock).thenReturn(clock)
+        currentClock.value = clock
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 070a0cc..57b5559 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -22,6 +22,7 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.res.R
@@ -31,6 +32,8 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
+import kotlinx.coroutines.flow.MutableStateFlow
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -46,6 +49,8 @@
     @Mock private lateinit var keyguardClockInteractor: KeyguardClockInteractor
     @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel
     @Mock private lateinit var splitShadeStateController: SplitShadeStateController
+    @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor>
+    private val clockShouldBeCentered: MutableStateFlow<Boolean> = MutableStateFlow(true)
 
     private lateinit var underTest: ClockSection
 
@@ -104,12 +109,15 @@
         whenever(packageManager.getResourcesForApplication(anyString())).thenReturn(remoteResources)
         mContext.setMockPackageManager(packageManager)
 
+        whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered)
+
         underTest =
             ClockSection(
                 keyguardClockInteractor,
                 keyguardClockViewModel,
                 mContext,
                 splitShadeStateController,
+                blueprintInteractor
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
index 28da957..deb3a83 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
@@ -26,6 +26,8 @@
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.res.R
@@ -34,7 +36,8 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.StateFlow
+import dagger.Lazy
+import kotlinx.coroutines.flow.MutableStateFlow
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -50,7 +53,8 @@
     @Mock private lateinit var keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel
     @Mock private lateinit var lockscreenSmartspaceController: LockscreenSmartspaceController
     @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
-    @Mock private lateinit var hasCustomWeatherDataDisplay: StateFlow<Boolean>
+    @Mock private lateinit var keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor
+    @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor>
 
     private val smartspaceView = View(mContext).also { it.id = sharedR.id.bc_smartspace_view }
     private val weatherView = View(mContext).also { it.id = sharedR.id.weather_smartspace_view }
@@ -58,17 +62,22 @@
     private lateinit var constraintLayout: ConstraintLayout
     private lateinit var constraintSet: ConstraintSet
 
+    private val clockShouldBeCentered = MutableStateFlow(false)
+    private val hasCustomWeatherDataDisplay = MutableStateFlow(false)
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
         mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
         underTest =
             SmartspaceSection(
+                mContext,
                 keyguardClockViewModel,
                 keyguardSmartspaceViewModel,
-                mContext,
+                keyguardSmartspaceInteractor,
                 lockscreenSmartspaceController,
                 keyguardUnlockAnimationController,
+                blueprintInteractor
             )
         constraintLayout = ConstraintLayout(mContext)
         whenever(lockscreenSmartspaceController.buildAndConnectView(any()))
@@ -78,6 +87,7 @@
         whenever(lockscreenSmartspaceController.buildAndConnectDateView(any())).thenReturn(dateView)
         whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay)
             .thenReturn(hasCustomWeatherDataDisplay)
+        whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered)
         constraintSet = ConstraintSet()
     }
 
@@ -115,7 +125,7 @@
     fun testConstraintsWhenNotHasCustomWeatherDataDisplay() {
         whenever(keyguardSmartspaceViewModel.isSmartspaceEnabled).thenReturn(true)
         whenever(keyguardSmartspaceViewModel.isDateWeatherDecoupled).thenReturn(true)
-        whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false)
+        hasCustomWeatherDataDisplay.value = false
         underTest.addViews(constraintLayout)
         underTest.applyConstraints(constraintSet)
         assertWeatherSmartspaceConstrains(constraintSet)
@@ -129,7 +139,7 @@
 
     @Test
     fun testConstraintsWhenHasCustomWeatherDataDisplay() {
-        whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true)
+        hasCustomWeatherDataDisplay.value = true
         underTest.addViews(constraintLayout)
         underTest.applyConstraints(constraintSet)
         assertWeatherSmartspaceConstrains(constraintSet)
@@ -140,7 +150,7 @@
 
     @Test
     fun testNormalDateWeatherVisibility() {
-        whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false)
+        hasCustomWeatherDataDisplay.value = false
         whenever(keyguardSmartspaceViewModel.isWeatherEnabled).thenReturn(true)
         underTest.addViews(constraintLayout)
         underTest.applyConstraints(constraintSet)
@@ -153,7 +163,7 @@
     }
     @Test
     fun testCustomDateWeatherVisibility() {
-        whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true)
+        hasCustomWeatherDataDisplay.value = true
         underTest.addViews(constraintLayout)
         underTest.applyConstraints(constraintSet)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
index 9941661..543f6c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt
@@ -16,17 +16,13 @@
 
 package com.android.systemui.scene.shared.flag
 
+import android.platform.test.annotations.DisableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.compose.ComposeFacade
-import com.android.systemui.flags.Flags
-import com.android.systemui.flags.setFlagValue
-import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
-import com.android.systemui.media.controls.util.MediaInSceneContainerFlag
+import com.android.systemui.flags.EnableSceneContainer
 import com.google.common.truth.Truth
-import org.junit.Assume
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -34,45 +30,17 @@
 @RunWith(AndroidJUnit4::class)
 internal class SceneContainerFlagsTest : SysuiTestCase() {
 
-    @Before
-    fun setUp() {
-        // TODO(b/283300105): remove this reflection setting once the hard-coded
-        //  Flags.SCENE_CONTAINER_ENABLED is no longer needed.
-        val field = Flags::class.java.getField("SCENE_CONTAINER_ENABLED")
-        field.isAccessible = true
-        field.set(null, true) // note: this does not work with multivalent tests
-    }
-
-    private fun setAconfigFlagsEnabled(enabled: Boolean) {
-        listOf(
-                com.android.systemui.Flags.FLAG_SCENE_CONTAINER,
-                com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
-                KeyguardShadeMigrationNssl.FLAG_NAME,
-                MediaInSceneContainerFlag.FLAG_NAME,
-            )
-            .forEach { flagName -> mSetFlagsRule.setFlagValue(flagName, enabled) }
-    }
-
     @Test
+    @DisableFlags(FLAG_SCENE_CONTAINER)
     fun isNotEnabled_withoutAconfigFlags() {
-        setAconfigFlagsEnabled(false)
         Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false)
         Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false)
     }
 
     @Test
-    fun isEnabled_withAconfigFlags_withCompose() {
-        Assume.assumeTrue(ComposeFacade.isComposeAvailable())
-        setAconfigFlagsEnabled(true)
+    @EnableSceneContainer
+    fun isEnabled_withAconfigFlags() {
         Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(true)
         Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(true)
     }
-
-    @Test
-    fun isNotEnabled_withAconfigFlags_withoutCompose() {
-        Assume.assumeFalse(ComposeFacade.isComposeAvailable())
-        setAconfigFlagsEnabled(true)
-        Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false)
-        Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false)
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 233cb3d..971c8a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -56,6 +56,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
@@ -67,6 +69,7 @@
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
 import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -128,7 +131,6 @@
     @Spy private final NotificationShadeWindowView mNotificationShadeWindowView = spy(
             new NotificationShadeWindowView(mContext, null));
     @Mock private IActivityManager mActivityManager;
-    @Mock private SysuiStatusBarStateController mStatusBarStateController;
     @Mock private ConfigurationController mConfigurationController;
     @Mock private KeyguardViewMediator mKeyguardViewMediator;
     @Mock private KeyguardBypassController mKeyguardBypassController;
@@ -137,7 +139,6 @@
     @Mock private DumpManager mDumpManager;
     @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
     @Mock private KeyguardStateController mKeyguardStateController;
-    @Mock private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock private AuthController mAuthController;
     @Mock private ShadeWindowLogger mShadeWindowLogger;
     @Mock private SelectedUserInteractor mSelectedUserInteractor;
@@ -157,6 +158,8 @@
     private float mPreferredRefreshRate = -1;
     private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor;
     private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor;
+    private ScreenOffAnimationController mScreenOffAnimationController;
+    private SysuiStatusBarStateController mStatusBarStateController;
 
     @Before
     public void setUp() {
@@ -175,11 +178,9 @@
         FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
         FakeShadeRepository shadeRepository = new FakeShadeRepository();
 
-        PowerInteractor powerInteractor = mUtils.powerInteractor(
-                mUtils.getPowerRepository(),
-                mUtils.falsingCollector(),
-                mScreenOffAnimationController,
-                mStatusBarStateController);
+        mScreenOffAnimationController = mUtils.getScreenOffAnimationController();
+        mStatusBarStateController = spy(mUtils.getStatusBarStateController());
+        PowerInteractor powerInteractor = mUtils.powerInteractor();
 
         SceneInteractor sceneInteractor = new SceneInteractor(
                 mTestScope.getBackgroundScope(),
@@ -200,6 +201,8 @@
                 new ConfigurationInteractor(configurationRepository),
                 shadeRepository,
                 () -> sceneInteractor);
+        CommunalInteractor communalInteractor =
+                CommunalInteractorFactory.create().getCommunalInteractor();
 
         FakeKeyguardTransitionRepository keyguardTransitionRepository =
                 new FakeKeyguardTransitionRepository();
@@ -222,6 +225,12 @@
                 featureFlags,
                 shadeRepository,
                 powerInteractor,
+                new GlanceableHubTransitions(
+                        mTestScope,
+                        keyguardTransitionInteractor,
+                        keyguardTransitionRepository,
+                        communalInteractor
+                ),
                 () ->
                         new InWindowLauncherUnlockAnimationInteractor(
                                 new InWindowLauncherUnlockAnimationRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index ee7c6c8..a11839c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.shade
 
 import android.content.Context
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.KeyEvent
@@ -25,50 +24,29 @@
 import android.view.View
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardMessageAreaController
 import com.android.keyguard.KeyguardSecurityContainerController
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
-import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
-import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
-import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.bouncer.ui.binder.BouncerViewBinder
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.compose.ComposeFacade.isComposeAvailable
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED
 import com.android.systemui.flags.Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION
 import com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON
 import com.android.systemui.flags.Flags.TRACKPAD_GESTURE_FEATURES
-import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler
-import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
-import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
-import com.android.systemui.log.BouncerLogger
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
@@ -86,16 +64,15 @@
 import com.android.systemui.statusbar.phone.DozeServiceHost
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
-import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
-import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.TestScope
@@ -110,9 +87,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.Optional
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -133,7 +109,6 @@
     @Mock private lateinit var shadeLogger: ShadeLogger
     @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var ambientState: AmbientState
-    @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
     @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController
     @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
     @Mock private lateinit var statusBarWindowStateController: StatusBarWindowStateController
@@ -156,8 +131,6 @@
     @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock lateinit var dragDownHelper: DragDownHelper
     @Mock lateinit var mSelectedUserInteractor: SelectedUserInteractor
-    @Mock
-    lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
     @Mock lateinit var sysUIKeyEventHandler: SysUIKeyEventHandler
     @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
@@ -224,55 +197,18 @@
                 dumpManager,
                 pulsingGestureListener,
                 mLockscreenHostedDreamGestureListener,
-                keyguardBouncerViewModel,
-                keyguardBouncerComponentFactory,
-                mock(KeyguardMessageAreaController.Factory::class.java),
                 keyguardTransitionInteractor,
-                primaryBouncerToGoneTransitionViewModel,
                 mGlanceableHubContainerController,
                 notificationLaunchAnimationInteractor,
                 featureFlagsClassic,
                 fakeClock,
-                BouncerMessageInteractor(
-                    repository = BouncerMessageRepositoryImpl(),
-                    userRepository = FakeUserRepository(),
-                    countDownTimerUtil = mock(CountDownTimerUtil::class.java),
-                    updateMonitor = mock(KeyguardUpdateMonitor::class.java),
-                    biometricSettingsRepository = FakeBiometricSettingsRepository(),
-                    applicationScope = testScope.backgroundScope,
-                    trustRepository = FakeTrustRepository(),
-                    systemPropertiesHelper = mock(SystemPropertiesHelper::class.java),
-                    primaryBouncerInteractor =
-                        PrimaryBouncerInteractor(
-                            FakeKeyguardBouncerRepository(),
-                            mock(BouncerView::class.java),
-                            mock(Handler::class.java),
-                            mock(KeyguardStateController::class.java),
-                            mock(KeyguardSecurityModel::class.java),
-                            mock(PrimaryBouncerCallbackInteractor::class.java),
-                            mock(FalsingCollector::class.java),
-                            mock(DismissCallbackRegistry::class.java),
-                            context,
-                            mock(KeyguardUpdateMonitor::class.java),
-                            FakeTrustRepository(),
-                            testScope.backgroundScope,
-                            mSelectedUserInteractor,
-                            mock(DeviceEntryFaceAuthInteractor::class.java)
-                        ),
-                    facePropertyRepository = FakeFacePropertyRepository(),
-                    deviceEntryFingerprintAuthRepository =
-                        FakeDeviceEntryFingerprintAuthRepository(),
-                    faceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
-                    securityModel = mock(KeyguardSecurityModel::class.java),
-                ),
-                BouncerLogger(logcatLogBuffer("BouncerLog")),
                 sysUIKeyEventHandler,
                 quickSettingsController,
                 primaryBouncerInteractor,
                 alternateBouncerInteractor,
-                mSelectedUserInteractor,
-                { mock (JavaAdapter::class.java )},
+                { mock(JavaAdapter::class.java) },
                 { mock(AlternateBouncerDependencies::class.java) },
+                mock(BouncerViewBinder::class.java)
             )
         underTest.setupExpandedStatusBar()
         underTest.setDragDownHelper(dragDownHelper)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 33d60ea..0c4bf81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -15,51 +15,28 @@
  */
 package com.android.systemui.shade
 
-import android.os.Handler
 import android.os.SystemClock
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.MotionEvent
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardMessageAreaController
 import com.android.keyguard.KeyguardSecurityContainerController
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
-import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
-import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
-import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler
-import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
-import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
-import com.android.systemui.log.BouncerLogger
-import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
@@ -77,11 +54,8 @@
 import com.android.systemui.statusbar.phone.DozeScrimController
 import com.android.systemui.statusbar.phone.DozeServiceHost
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
-import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -137,7 +111,6 @@
     @Mock private lateinit var pulsingGestureListener: PulsingGestureListener
     @Mock
     private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
-    @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
     @Mock private lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock private lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
     @Mock
@@ -150,10 +123,6 @@
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
-    @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
-    @Mock
-    private lateinit var primaryBouncerToGoneTransitionViewModel:
-        PrimaryBouncerToGoneTransitionViewModel
     @Captor
     private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
 
@@ -217,55 +186,18 @@
                 dumpManager,
                 pulsingGestureListener,
                 mLockscreenHostedDreamGestureListener,
-                keyguardBouncerViewModel,
-                keyguardBouncerComponentFactory,
-                Mockito.mock(KeyguardMessageAreaController.Factory::class.java),
                 keyguardTransitionInteractor,
-                primaryBouncerToGoneTransitionViewModel,
                 mGlanceableHubContainerController,
                 NotificationLaunchAnimationInteractor(NotificationLaunchAnimationRepository()),
                 featureFlags,
                 FakeSystemClock(),
-                BouncerMessageInteractor(
-                    repository = BouncerMessageRepositoryImpl(),
-                    userRepository = FakeUserRepository(),
-                    countDownTimerUtil = Mockito.mock(CountDownTimerUtil::class.java),
-                    updateMonitor = Mockito.mock(KeyguardUpdateMonitor::class.java),
-                    biometricSettingsRepository = FakeBiometricSettingsRepository(),
-                    applicationScope = testScope.backgroundScope,
-                    trustRepository = FakeTrustRepository(),
-                    systemPropertiesHelper = Mockito.mock(SystemPropertiesHelper::class.java),
-                    primaryBouncerInteractor =
-                        PrimaryBouncerInteractor(
-                            FakeKeyguardBouncerRepository(),
-                            Mockito.mock(BouncerView::class.java),
-                            Mockito.mock(Handler::class.java),
-                            Mockito.mock(KeyguardStateController::class.java),
-                            Mockito.mock(KeyguardSecurityModel::class.java),
-                            Mockito.mock(PrimaryBouncerCallbackInteractor::class.java),
-                            Mockito.mock(FalsingCollector::class.java),
-                            Mockito.mock(DismissCallbackRegistry::class.java),
-                            context,
-                            Mockito.mock(KeyguardUpdateMonitor::class.java),
-                            FakeTrustRepository(),
-                            testScope.backgroundScope,
-                            mSelectedUserInteractor,
-                            mock(),
-                        ),
-                    facePropertyRepository = FakeFacePropertyRepository(),
-                    deviceEntryFingerprintAuthRepository =
-                        FakeDeviceEntryFingerprintAuthRepository(),
-                    faceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
-                    securityModel = Mockito.mock(KeyguardSecurityModel::class.java),
-                ),
-                BouncerLogger(logcatLogBuffer("BouncerLog")),
                 Mockito.mock(SysUIKeyEventHandler::class.java),
                 quickSettingsController,
                 primaryBouncerInteractor,
                 alternateBouncerInteractor,
-                mSelectedUserInteractor,
                 { Mockito.mock(JavaAdapter::class.java) },
                 { Mockito.mock(AlternateBouncerDependencies::class.java) },
+                mock()
             )
 
         controller.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
index ea3caa3..697b05a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
@@ -26,6 +26,7 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
@@ -167,10 +168,15 @@
     }
 
     @Test
-    fun testLargeScreen_updateResources_splitShadeHeightIsSet() {
+    fun testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSetBasedOnResource() {
+        val headerResourceHeight = 20
+        val headerHelperHeight = 30
+        mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+        whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+            .thenReturn(headerHelperHeight)
         overrideResource(R.bool.config_use_large_screen_shade_header, true)
         overrideResource(R.dimen.qs_header_height, 10)
-        overrideResource(R.dimen.large_screen_shade_header_height, 20)
+        overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
 
         // ensure the estimated height (would be 3 here) wouldn't impact this test case
         overrideResource(R.dimen.large_screen_shade_header_min_height, 1)
@@ -180,7 +186,31 @@
 
         val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
         verify(view).applyConstraints(capture(captor))
-        assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(20)
+        assertThat(captor.value.getHeight(R.id.split_shade_status_bar))
+            .isEqualTo(headerResourceHeight)
+    }
+
+    @Test
+    fun testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSetBasedOnHelper() {
+        val headerResourceHeight = 20
+        val headerHelperHeight = 30
+        mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+        whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+            .thenReturn(headerHelperHeight)
+        overrideResource(R.bool.config_use_large_screen_shade_header, true)
+        overrideResource(R.dimen.qs_header_height, 10)
+        overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
+
+        // ensure the estimated height (would be 3 here) wouldn't impact this test case
+        overrideResource(R.dimen.large_screen_shade_header_min_height, 1)
+        overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1)
+
+        underTest.updateResources()
+
+        val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
+        verify(view).applyConstraints(capture(captor))
+        assertThat(captor.value.getHeight(R.id.split_shade_status_bar))
+            .isEqualTo(headerHelperHeight)
     }
 
     @Test
@@ -416,10 +446,14 @@
     }
 
     @Test
-    fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() {
+    fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResource() {
+        mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
         setLargeScreen()
-        val largeScreenHeaderHeight = 100
-        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight)
+        val largeScreenHeaderResourceHeight = 100
+        val largeScreenHeaderHelperHeight = 200
+        whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+            .thenReturn(largeScreenHeaderHelperHeight)
+        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight)
 
         // ensure the estimated height (would be 30 here) wouldn't impact this test case
         overrideResource(R.dimen.large_screen_shade_header_min_height, 10)
@@ -428,9 +462,31 @@
         underTest.updateResources()
 
         assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
-            .isEqualTo(largeScreenHeaderHeight)
+            .isEqualTo(largeScreenHeaderResourceHeight)
         assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
-            .isEqualTo(largeScreenHeaderHeight)
+            .isEqualTo(largeScreenHeaderResourceHeight)
+    }
+
+    @Test
+    fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelper() {
+        mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+        setLargeScreen()
+        val largeScreenHeaderResourceHeight = 100
+        val largeScreenHeaderHelperHeight = 200
+        whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+            .thenReturn(largeScreenHeaderHelperHeight)
+        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight)
+
+        // ensure the estimated height (would be 30 here) wouldn't impact this test case
+        overrideResource(R.dimen.large_screen_shade_header_min_height, 10)
+        overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10)
+
+        underTest.updateResources()
+
+        assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
+            .isEqualTo(largeScreenHeaderHelperHeight)
+        assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin)
+            .isEqualTo(largeScreenHeaderHelperHeight)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
index c1bc303..e66251a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
@@ -26,6 +26,7 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
@@ -166,10 +167,14 @@
     }
 
     @Test
-    fun testLargeScreen_updateResources_splitShadeHeightIsSet() {
+    fun testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSet_basedOnResource() {
+        mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+        val helperHeight = 30
+        val resourceHeight = 20
+        whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(helperHeight)
         overrideResource(R.bool.config_use_large_screen_shade_header, true)
         overrideResource(R.dimen.qs_header_height, 10)
-        overrideResource(R.dimen.large_screen_shade_header_height, 20)
+        overrideResource(R.dimen.large_screen_shade_header_height, resourceHeight)
 
         // ensure the estimated height (would be 3 here) wouldn't impact this test case
         overrideResource(R.dimen.large_screen_shade_header_min_height, 1)
@@ -179,7 +184,28 @@
 
         val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
         verify(view).applyConstraints(capture(captor))
-        assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(20)
+        assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(resourceHeight)
+    }
+
+    @Test
+    fun testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSet_basedOnHelper() {
+        mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+        val helperHeight = 30
+        val resourceHeight = 20
+        whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(helperHeight)
+        overrideResource(R.bool.config_use_large_screen_shade_header, true)
+        overrideResource(R.dimen.qs_header_height, 10)
+        overrideResource(R.dimen.large_screen_shade_header_height, resourceHeight)
+
+        // ensure the estimated height (would be 3 here) wouldn't impact this test case
+        overrideResource(R.dimen.large_screen_shade_header_min_height, 1)
+        overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1)
+
+        underTest.updateResources()
+
+        val captor = ArgumentCaptor.forClass(ConstraintSet::class.java)
+        verify(view).applyConstraints(capture(captor))
+        assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(helperHeight)
     }
 
     @Test
@@ -404,10 +430,14 @@
     }
 
     @Test
-    fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() {
+    fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderResourceHeight() {
+        mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
         setLargeScreen()
-        val largeScreenHeaderHeight = 100
-        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight)
+        val largeScreenHeaderHelperHeight = 200
+        val largeScreenHeaderResourceHeight = 100
+        whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+            .thenReturn(largeScreenHeaderHelperHeight)
+        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight)
 
         // ensure the estimated height (would be 30 here) wouldn't impact this test case
         overrideResource(R.dimen.large_screen_shade_header_min_height, 10)
@@ -416,7 +446,27 @@
         underTest.updateResources()
 
         assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
-            .isEqualTo(largeScreenHeaderHeight)
+            .isEqualTo(largeScreenHeaderResourceHeight)
+    }
+
+    @Test
+    fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHelperHeight() {
+        mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+        setLargeScreen()
+        val largeScreenHeaderHelperHeight = 200
+        val largeScreenHeaderResourceHeight = 100
+        whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+            .thenReturn(largeScreenHeaderHelperHeight)
+        overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight)
+
+        // ensure the estimated height (would be 30 here) wouldn't impact this test case
+        overrideResource(R.dimen.large_screen_shade_header_min_height, 10)
+        overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10)
+
+        underTest.updateResources()
+
+        assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin)
+            .isEqualTo(largeScreenHeaderHelperHeight)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index f0a2303..ca68fd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -42,6 +42,8 @@
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
@@ -55,6 +57,7 @@
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
 import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -82,7 +85,6 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.QsFrameTranslateController;
-import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository;
@@ -96,7 +98,6 @@
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
@@ -169,7 +170,6 @@
     @Mock protected LockscreenGestureLogger mLockscreenGestureLogger;
     @Mock protected MetricsLogger mMetricsLogger;
     @Mock protected FeatureFlags mFeatureFlags;
-    @Mock protected InteractionJankMonitor mInteractionJankMonitor;
     @Mock protected ShadeLogger mShadeLogger;
     @Mock protected DumpManager mDumpManager;
     @Mock protected UiEventLogger mUiEventLogger;
@@ -183,6 +183,7 @@
     protected FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository();
     protected FakeShadeRepository mShadeRepository = new FakeShadeRepository();
 
+    protected InteractionJankMonitor mInteractionJankMonitor;
     protected SysuiStatusBarStateController mStatusBarStateController;
     protected ShadeInteractor mShadeInteractor;
 
@@ -202,8 +203,8 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mPanelViewControllerLazy.get()).thenReturn(mNotificationPanelViewController);
-        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger,
-                mInteractionJankMonitor, mock(JavaAdapter.class), () -> mShadeInteractor);
+        mStatusBarStateController = mUtils.getStatusBarStateController();
+        mInteractionJankMonitor = mUtils.getInteractionJankMonitor();
 
         FakeDeviceProvisioningRepository deviceProvisioningRepository =
                 new FakeDeviceProvisioningRepository();
@@ -211,11 +212,7 @@
         FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
         FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository();
 
-        PowerInteractor powerInteractor = mUtils.powerInteractor(
-                mUtils.getPowerRepository(),
-                mUtils.falsingCollector(),
-                mock(ScreenOffAnimationController.class),
-                mStatusBarStateController);
+        PowerInteractor powerInteractor = mUtils.powerInteractor();
 
         SceneInteractor sceneInteractor = new SceneInteractor(
                 mTestScope.getBackgroundScope(),
@@ -235,6 +232,8 @@
                 new ConfigurationInteractor(configurationRepository),
                 mShadeRepository,
                 () -> sceneInteractor);
+        CommunalInteractor communalInteractor =
+                CommunalInteractorFactory.create().getCommunalInteractor();
 
         FakeKeyguardTransitionRepository keyguardTransitionRepository =
                 new FakeKeyguardTransitionRepository();
@@ -257,6 +256,12 @@
                 featureFlags,
                 mShadeRepository,
                 powerInteractor,
+                new GlanceableHubTransitions(
+                        mTestScope,
+                        keyguardTransitionInteractor,
+                        keyguardTransitionRepository,
+                        communalInteractor
+                ),
                 () ->
                         new InWindowLauncherUnlockAnimationInteractor(
                                 new InWindowLauncherUnlockAnimationRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 1a6a067..4a365b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
@@ -36,6 +37,7 @@
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions
 import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -139,6 +141,7 @@
                 { fromLockscreenTransitionInteractor },
                 { fromPrimaryBouncerTransitionInteractor }
             )
+        val communalInteractor = CommunalInteractorFactory.create().communalInteractor
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                 keyguardTransitionRepository,
@@ -150,6 +153,12 @@
                 featureFlags,
                 shadeRepository,
                 powerInteractor,
+                GlanceableHubTransitions(
+                    testScope,
+                    keyguardTransitionInteractor,
+                    keyguardTransitionRepository,
+                    communalInteractor
+                ),
                 {
                     InWindowLauncherUnlockAnimationInteractor(
                         InWindowLauncherUnlockAnimationRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index 4ab3cd4..9b641f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -50,6 +50,18 @@
         DaggerActiveNotificationsInteractorTest_TestComponent.factory().create(test = this)
 
     @Test
+    fun testAllNotificationsCount() =
+        testComponent.runTest {
+            val count by collectLastValue(underTest.allNotificationsCount)
+
+            activeNotificationListRepository.setActiveNotifs(5)
+            runCurrent()
+
+            assertThat(count).isEqualTo(5)
+            assertThat(underTest.allNotificationsCountValue).isEqualTo(5)
+        }
+
+    @Test
     fun testAreAnyNotificationsPresent_isTrue() =
         testComponent.runTest {
             val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
@@ -74,6 +86,18 @@
         }
 
     @Test
+    fun testActiveNotificationRanks_sizeMatches() {
+        testComponent.runTest {
+            val activeNotificationRanks by collectLastValue(underTest.activeNotificationRanks)
+
+            activeNotificationListRepository.setActiveNotifs(5)
+            runCurrent()
+
+            assertThat(activeNotificationRanks!!.size).isEqualTo(5)
+        }
+    }
+
+    @Test
     fun testHasClearableNotifications_whenHasClearableAlertingNotifs() =
         testComponent.runTest {
             val hasClearable by collectLastValue(underTest.hasClearableNotifications)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
index 6374d5e..334776c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
@@ -15,10 +15,12 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import android.service.notification.StatusBarNotification
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.byKey
@@ -49,22 +51,101 @@
         testScope.runTest {
             val notifs by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications)
             val keys = (1..50).shuffled().map { "$it" }
-            val entries =
-                keys.map {
-                    mock<ListEntry> {
-                        val mockRep =
-                            mock<NotificationEntry> {
-                                whenever(key).thenReturn(it)
-                                whenever(sbn).thenReturn(mock())
-                                whenever(icons).thenReturn(mock())
-                            }
-                        whenever(representativeEntry).thenReturn(mockRep)
-                    }
-                }
+            val entries = keys.map { mockNotificationEntry(key = it) }
             underTest.setRenderedList(entries)
             assertThat(notifs)
                 .comparingElementsUsing(byKey)
                 .containsExactlyElementsIn(keys)
                 .inOrder()
         }
+
+    @Test
+    fun setRenderList_flatMapsRankings() =
+        testScope.runTest {
+            val ranks by collectLastValue(notifsInteractor.activeNotificationRanks)
+
+            val single = mockNotificationEntry("single", 0)
+            val group =
+                mockGroupEntry(
+                    key = "group",
+                    summary = mockNotificationEntry("summary", 1),
+                    children =
+                        listOf(
+                            mockNotificationEntry("child0", 2),
+                            mockNotificationEntry("child1", 3),
+                        ),
+                )
+
+            underTest.setRenderedList(listOf(single, group))
+
+            assertThat(ranks)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        "single" to 0,
+                        "summary" to 1,
+                        "child0" to 2,
+                        "child1" to 3,
+                    )
+                )
+        }
+
+    @Test
+    fun setRenderList_singleItems_mapsRankings() =
+        testScope.runTest {
+            val actual by collectLastValue(notifsInteractor.activeNotificationRanks)
+            val expected =
+                (0..10).shuffled().mapIndexed { index, value -> "$value" to index }.toMap()
+
+            val entries = expected.map { (key, rank) -> mockNotificationEntry(key, rank) }
+
+            underTest.setRenderedList(entries)
+
+            assertThat(actual).containsAtLeastEntriesIn(expected)
+        }
+
+    @Test
+    fun setRenderList_groupWithNoSummary_flatMapsRankings() =
+        testScope.runTest {
+            val actual by collectLastValue(notifsInteractor.activeNotificationRanks)
+            val expected =
+                (0..10).shuffled().mapIndexed { index, value -> "$value" to index }.toMap()
+
+            val group =
+                mockGroupEntry(
+                    key = "group",
+                    summary = null,
+                    children = expected.map { (key, rank) -> mockNotificationEntry(key, rank) },
+                )
+
+            underTest.setRenderedList(listOf(group))
+
+            assertThat(actual).containsAtLeastEntriesIn(expected)
+        }
+}
+
+private fun mockGroupEntry(
+    key: String,
+    summary: NotificationEntry?,
+    children: List<NotificationEntry>,
+): GroupEntry {
+    return mock<GroupEntry> {
+        whenever(this.key).thenReturn(key)
+        whenever(this.summary).thenReturn(summary)
+        whenever(this.children).thenReturn(children)
+    }
+}
+
+private fun mockNotificationEntry(key: String, rank: Int = 0): NotificationEntry {
+    val mockSbn =
+        mock<StatusBarNotification>() {
+            whenever(notification).thenReturn(mock())
+            whenever(packageName).thenReturn("com.android")
+        }
+    return mock<NotificationEntry> {
+        whenever(this.key).thenReturn(key)
+        whenever(this.icons).thenReturn(mock())
+        whenever(this.representativeEntry).thenReturn(this)
+        whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build())
+        whenever(this.sbn).thenReturn(mockSbn)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
index dae0aa2..d61fc05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
@@ -34,6 +34,11 @@
     }
 
     @Override
+    public void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto) {
+        mCalls.add(new CallRecord(isLockscreen, proto));
+    }
+
+    @Override
     public void logPanelShown(boolean isLockscreen,
             List<NotificationEntry> visibleNotifications) {
         mCalls.add(new CallRecord(isLockscreen,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
index 9547af1..8ac2a33 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
@@ -40,12 +40,12 @@
 import com.android.systemui.statusbar.notification.collection.render.FakeNodeController
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
-import com.android.systemui.statusbar.notification.logging.NotificationLogger
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController.BUBBLES_SETTING_URI
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.SmartReplyConstants
@@ -92,7 +92,7 @@
     private val groupMembershipManager: GroupMembershipManager = mock()
     private val groupExpansionManager: GroupExpansionManager = mock()
     private val rowContentBindStage: RowContentBindStage = mock()
-    private val notifLogger: NotificationLogger = mock()
+    private val notifLogger: NotificationRowStatsLogger = mock()
     private val headsUpManager: HeadsUpManager = mock()
     private val onExpandClickListener: ExpandableNotificationRow.OnExpandClickListener = mock()
     private val statusBarStateController: StatusBarStateController = mock()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 5549fee..91e4666 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.notification.FeedbackIcon
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import junit.framework.Assert.assertEquals
@@ -49,6 +50,8 @@
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
@@ -74,7 +77,8 @@
     @Before
     fun setup() {
         initMocks(this)
-        fakeParent = FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE }
+        fakeParent =
+            spy(FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE })
         row =
             spy(
                 ExpandableNotificationRow(mContext, /* attrs= */ null).apply {
@@ -558,6 +562,35 @@
         verify(view.headsUpWrapper, never()).setAnimationsRunning(false)
     }
 
+    @Test
+    fun notifySubtreeAccessibilityStateChanged_notifiesParent() {
+        // Given: a contentView is created
+        val view = createContentView()
+        clearInvocations(fakeParent)
+
+        // When: the contentView is notified for an A11y change
+        view.notifySubtreeAccessibilityStateChanged(view.contractedChild, view.contractedChild, 0)
+
+        // Then: the contentView propagates the event to its parent
+        verify(fakeParent).notifySubtreeAccessibilityStateChanged(any(), any(), anyInt())
+    }
+
+    @Test
+    fun notifySubtreeAccessibilityStateChanged_animatingContentView_dontNotifyParent() {
+        // Given: a collapsed contentView is created
+        val view = createContentView()
+        clearInvocations(fakeParent)
+
+        // And: it is animating to expanded
+        view.setAnimationStartVisibleType(NotificationContentView.VISIBLE_TYPE_EXPANDED)
+
+        // When: the contentView is notified for an A11y change
+        view.notifySubtreeAccessibilityStateChanged(view.contractedChild, view.contractedChild, 0)
+
+        // Then: the contentView DOESN'T propagates the event to its parent
+        verify(fakeParent, never()).notifySubtreeAccessibilityStateChanged(any(), any(), anyInt())
+    }
+
     private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
         mock<ExpandableNotificationRow>().apply {
             whenever(this.entry).thenReturn(notificationEntry)
@@ -597,7 +630,7 @@
         }
 
     private fun createContentView(
-        isSystemExpanded: Boolean,
+        isSystemExpanded: Boolean = false,
         contractedView: View = createViewWithHeight(contractedHeight),
         expandedView: View = createViewWithHeight(expandedHeight),
         headsUpView: View = createViewWithHeight(contractedHeight),
@@ -647,5 +680,5 @@
 }
 
 private fun NotificationContentView.clearInvocations() {
-    Mockito.clearInvocations(contractedWrapper, expandedWrapper, headsUpWrapper)
+    clearInvocations(contractedWrapper, expandedWrapper, headsUpWrapper)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt
new file mode 100644
index 0000000..1dfcb38
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.statusbar.notification.stack
+
+import androidx.test.filters.SmallTest
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class DisplaySwitchNotificationsHiderTrackerTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val shadeInteractor = mock<ShadeInteractor>()
+    private val latencyTracker = mock<LatencyTracker>()
+
+    private val shouldHideFlow = MutableStateFlow(false)
+    private val shadeExpandedFlow = MutableStateFlow(false)
+
+    private val tracker = DisplaySwitchNotificationsHiderTracker(shadeInteractor, latencyTracker)
+
+    @Before
+    fun setup() {
+        whenever(shadeInteractor.isAnyExpanded).thenReturn(shadeExpandedFlow)
+    }
+
+    @Test
+    fun notificationsBecomeHidden_tracksHideActionStart() = testScope.runTest {
+        startTracking()
+
+        shouldHideFlow.value = true
+        runCurrent()
+
+        verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_ACTION)
+    }
+
+    @Test
+    fun notificationsBecomeVisibleAfterHidden_tracksHideActionEnd() = testScope.runTest {
+        startTracking()
+
+        shouldHideFlow.value = true
+        runCurrent()
+        clearInvocations(latencyTracker)
+        shouldHideFlow.value = false
+        runCurrent()
+
+        verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_ACTION)
+    }
+
+    @Test
+    fun notificationsBecomeHiddenWhenShadeIsClosed_doesNotTrackHideWhenVisibleActionStart() =
+            testScope.runTest {
+                shouldHideFlow.value = false
+                shadeExpandedFlow.value = false
+                startTracking()
+
+                shouldHideFlow.value = true
+                runCurrent()
+
+                verify(latencyTracker, never())
+                        .onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+            }
+
+    @Test
+    fun notificationsBecomeHiddenWhenShadeIsOpen_tracksHideWhenVisibleActionStart() = testScope.runTest {
+        shouldHideFlow.value = false
+        shadeExpandedFlow.value = false
+        startTracking()
+
+        shouldHideFlow.value = true
+        shadeExpandedFlow.value = true
+        runCurrent()
+
+        verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+    }
+
+    @Test
+    fun shadeBecomesOpenWhenNotificationsHidden_tracksHideWhenVisibleActionStart() =
+            testScope.runTest {
+            shouldHideFlow.value = true
+            shadeExpandedFlow.value = false
+            startTracking()
+
+            shadeExpandedFlow.value = true
+            runCurrent()
+
+            verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+        }
+
+    @Test
+    fun notificationsBecomeVisibleWhenShadeIsOpen_tracksHideWhenVisibleActionEnd() = testScope.runTest {
+        shouldHideFlow.value = false
+        shadeExpandedFlow.value = false
+        startTracking()
+        shouldHideFlow.value = true
+        shadeExpandedFlow.value = true
+        runCurrent()
+        clearInvocations(latencyTracker)
+
+        shouldHideFlow.value = false
+        runCurrent()
+
+        verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+    }
+
+    @Test
+    fun shadeBecomesClosedWhenNotificationsHidden_tracksHideWhenVisibleActionEnd() = testScope.runTest {
+        shouldHideFlow.value = false
+        shadeExpandedFlow.value = false
+        startTracking()
+        shouldHideFlow.value = true
+        shadeExpandedFlow.value = true
+        runCurrent()
+        clearInvocations(latencyTracker)
+
+        shadeExpandedFlow.value = false
+        runCurrent()
+
+        verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+    }
+
+    private fun TestScope.startTracking() {
+        backgroundScope.launch { tracker.trackNotificationHideTime(shouldHideFlow) }
+        backgroundScope.launch { tracker.trackNotificationHideTimeWhenVisible(shouldHideFlow) }
+        runCurrent()
+        clearInvocations(latencyTracker)
+    }
+
+    private companion object {
+        const val HIDE_NOTIFICATIONS_ACTION = ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+        const val HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION =
+                ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt
new file mode 100644
index 0000000..d7d1ffc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import android.service.notification.notificationListenerService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.statusbar.NotificationVisibility
+import com.android.internal.statusbar.statusBarService
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.logging.nano.Notifications
+import com.android.systemui.statusbar.notification.logging.notificationPanelLogger
+import com.android.systemui.statusbar.notification.stack.ExpandableViewState
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Callable
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationStatsLoggerTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private val testScope = kosmos.testScope
+    private val mockNotificationListenerService = kosmos.notificationListenerService
+    private val mockPanelLogger = kosmos.notificationPanelLogger
+    private val mockStatusBarService = kosmos.statusBarService
+
+    private val underTest = kosmos.notificationStatsLogger
+
+    private val visibilityArrayCaptor = argumentCaptor<Array<NotificationVisibility>>()
+    private val stringArrayCaptor = argumentCaptor<Array<String>>()
+    private val notificationListProtoCaptor = argumentCaptor<Notifications.NotificationList>()
+
+    @Test
+    fun onNotificationListUpdated_itemsAdded_logsNewlyVisibleItems() =
+        testScope.runTest {
+            // WHEN new Notifications are added
+            // AND they're visible
+            val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+            val callable = Callable { locations }
+            underTest.onNotificationListUpdated(callable, ranks)
+            runCurrent()
+
+            // THEN visibility changes are reported
+            verify(mockStatusBarService)
+                .onNotificationVisibilityChanged(visibilityArrayCaptor.capture(), eq(emptyArray()))
+            verify(mockNotificationListenerService)
+                .setNotificationsShown(stringArrayCaptor.capture())
+            val loggedVisibilities = visibilityArrayCaptor.value
+            val loggedKeys = stringArrayCaptor.value
+            assertThat(loggedVisibilities).hasLength(2)
+            assertThat(loggedKeys).hasLength(2)
+            assertThat(loggedVisibilities[0]).apply {
+                isKeyEqualTo("key0")
+                isRankEqualTo(0)
+                isVisible()
+                isInMainArea()
+                isCountEqualTo(2)
+            }
+            assertThat(loggedVisibilities[1]).apply {
+                isKeyEqualTo("key1")
+                isRankEqualTo(1)
+                isVisible()
+                isInMainArea()
+                isCountEqualTo(2)
+            }
+            assertThat(loggedKeys[0]).isEqualTo("key0")
+            assertThat(loggedKeys[1]).isEqualTo("key1")
+        }
+
+    @Test
+    fun onNotificationListUpdated_itemsRemoved_logsNoLongerVisibleItems() =
+        testScope.runTest {
+            // GIVEN some visible Notifications are reported
+            val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+            val callable = Callable { locations }
+            underTest.onNotificationListUpdated(callable, ranks)
+            runCurrent()
+            clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+            // WHEN the same Notifications are removed
+            val emptyCallable = Callable { emptyMap<String, Int>() }
+            underTest.onNotificationListUpdated(emptyCallable, emptyMap())
+            runCurrent()
+
+            // THEN visibility changes are reported
+            verify(mockStatusBarService)
+                .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture())
+            verifyZeroInteractions(mockNotificationListenerService)
+            val noLongerVisible = visibilityArrayCaptor.value
+            assertThat(noLongerVisible).hasLength(2)
+            assertThat(noLongerVisible[0]).apply {
+                isKeyEqualTo("key0")
+                isRankEqualTo(0)
+                notVisible()
+                isInMainArea()
+                isCountEqualTo(0)
+            }
+            assertThat(noLongerVisible[1]).apply {
+                isKeyEqualTo("key1")
+                isRankEqualTo(1)
+                notVisible()
+                isInMainArea()
+                isCountEqualTo(0)
+            }
+        }
+
+    @Test
+    fun onNotificationListUpdated_itemsBecomeInvisible_logsNoLongerVisibleItems() =
+        testScope.runTest {
+            // GIVEN some visible Notifications are reported
+            val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+            val callable = Callable { locations }
+            underTest.onNotificationListUpdated(callable, ranks)
+            runCurrent()
+            clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+            // WHEN the same Notifications are becoming invisible
+            val emptyCallable = Callable { emptyMap<String, Int>() }
+            underTest.onNotificationListUpdated(emptyCallable, ranks)
+            runCurrent()
+
+            // THEN visibility changes are reported
+            verify(mockStatusBarService)
+                .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture())
+            verifyZeroInteractions(mockNotificationListenerService)
+            val noLongerVisible = visibilityArrayCaptor.value
+            assertThat(noLongerVisible).hasLength(2)
+            assertThat(noLongerVisible[0]).apply {
+                isKeyEqualTo("key0")
+                isRankEqualTo(0)
+                notVisible()
+                isInMainArea()
+                isCountEqualTo(2)
+            }
+            assertThat(noLongerVisible[1]).apply {
+                isKeyEqualTo("key1")
+                isRankEqualTo(1)
+                notVisible()
+                isInMainArea()
+                isCountEqualTo(2)
+            }
+        }
+
+    @Test
+    fun onNotificationListUpdated_itemsChangedPositions_nothingLogged() =
+        testScope.runTest {
+            // GIVEN some visible Notifications are reported
+            val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+            underTest.onNotificationListUpdated({ locations }, ranks)
+            runCurrent()
+            clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+            // WHEN the reported Notifications are changing positions
+            val (newRanks, newLocations) = fakeNotificationMaps("key1", "key0")
+            underTest.onNotificationListUpdated({ newLocations }, newRanks)
+            runCurrent()
+
+            // THEN no visibility changes are reported
+            verifyZeroInteractions(mockStatusBarService, mockNotificationListenerService)
+        }
+
+    @Test
+    fun onNotificationListUpdated_calledTwice_usesTheNewCallable() =
+        testScope.runTest {
+            // GIVEN some visible Notifications are reported
+            val (ranks, locations) = fakeNotificationMaps("key0", "key1", "key2")
+            val callable = spy(Callable { locations })
+            underTest.onNotificationListUpdated(callable, ranks)
+            runCurrent()
+            clearInvocations(callable)
+
+            // WHEN a new update comes
+            val otherCallable = spy(Callable { locations })
+            underTest.onNotificationListUpdated(otherCallable, ranks)
+            runCurrent()
+
+            // THEN we call the new Callable
+            verifyZeroInteractions(callable)
+            verify(otherCallable).call()
+        }
+
+    @Test
+    fun onLockscreenOrShadeNotInteractive_logsNoLongerVisibleItems() =
+        testScope.runTest {
+            // GIVEN some visible Notifications are reported
+            val (ranks, locations) = fakeNotificationMaps("key0", "key1")
+            val callable = Callable { locations }
+            underTest.onNotificationListUpdated(callable, ranks)
+            runCurrent()
+            clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+            // WHEN the Shade becomes non interactive
+            underTest.onLockscreenOrShadeNotInteractive(emptyList())
+            runCurrent()
+
+            // THEN visibility changes are reported
+            verify(mockStatusBarService)
+                .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture())
+            verifyZeroInteractions(mockNotificationListenerService)
+            val noLongerVisible = visibilityArrayCaptor.value
+            assertThat(noLongerVisible).hasLength(2)
+            assertThat(noLongerVisible[0]).apply {
+                isKeyEqualTo("key0")
+                isRankEqualTo(0)
+                notVisible()
+                isInMainArea()
+                isCountEqualTo(0)
+            }
+            assertThat(noLongerVisible[1]).apply {
+                isKeyEqualTo("key1")
+                isRankEqualTo(1)
+                notVisible()
+                isInMainArea()
+                isCountEqualTo(0)
+            }
+        }
+
+    @Test
+    fun onLockscreenOrShadeInteractive_logsPanelShown() =
+        testScope.runTest {
+            // WHEN the Shade becomes interactive
+            underTest.onLockscreenOrShadeInteractive(
+                isOnLockScreen = true,
+                listOf(
+                    activeNotificationModel(
+                        key = "key0",
+                        uid = 0,
+                        packageName = "com.android.first"
+                    ),
+                    activeNotificationModel(
+                        key = "key1",
+                        uid = 1,
+                        packageName = "com.android.second"
+                    ),
+                )
+            )
+            runCurrent()
+
+            // THEN the Panel shown event is reported
+            verify(mockPanelLogger).logPanelShown(eq(true), notificationListProtoCaptor.capture())
+            val loggedNotifications = notificationListProtoCaptor.value.notifications
+            assertThat(loggedNotifications.size).isEqualTo(2)
+            with(loggedNotifications[0]) {
+                assertThat(uid).isEqualTo(0)
+                assertThat(packageName).isEqualTo("com.android.first")
+            }
+            with(loggedNotifications[1]) {
+                assertThat(uid).isEqualTo(1)
+                assertThat(packageName).isEqualTo("com.android.second")
+            }
+        }
+
+    @Test
+    fun onNotificationExpanded_visibleLocation_expansionLogged() =
+        testScope.runTest {
+            // WHEN a Notification is expanded
+            underTest.onNotificationExpansionChanged(
+                key = "key",
+                isExpanded = true,
+                location = ExpandableViewState.LOCATION_MAIN_AREA,
+                isUserAction = true
+            )
+            runCurrent()
+
+            // THEN the Expand event is reported
+            verify(mockStatusBarService)
+                .onNotificationExpansionChanged(
+                    /* key = */ "key",
+                    /* userAction = */ true,
+                    /* expanded = */ true,
+                    NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+                )
+        }
+
+    @Test
+    fun onNotificationExpanded_notVisibleLocation_nothingLogged() =
+        testScope.runTest {
+            // WHEN a NOT visible Notification is expanded
+            underTest.onNotificationExpansionChanged(
+                key = "key",
+                isExpanded = true,
+                location = ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN,
+                isUserAction = true
+            )
+            runCurrent()
+
+            // No events are reported
+            verifyZeroInteractions(mockStatusBarService)
+        }
+
+    @Test
+    fun onNotificationExpanded_notVisibleLocation_becomesVisible_expansionLogged() =
+        testScope.runTest {
+            // WHEN a NOT visible Notification is expanded
+            underTest.onNotificationExpansionChanged(
+                key = "key",
+                isExpanded = true,
+                location = ExpandableViewState.LOCATION_GONE,
+                isUserAction = true
+            )
+            runCurrent()
+
+            // AND it becomes visible
+            val (ranks, locations) = fakeNotificationMaps("key")
+            val callable = Callable { locations }
+            underTest.onNotificationListUpdated(callable, ranks)
+            runCurrent()
+
+            // THEN the Expand event is reported
+            verify(mockStatusBarService)
+                .onNotificationExpansionChanged(
+                    /* key = */ "key",
+                    /* userAction = */ true,
+                    /* expanded = */ true,
+                    NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+                )
+        }
+
+    @Test
+    fun onNotificationCollapsed_isFirstInteraction_nothingLogged() =
+        testScope.runTest {
+            // WHEN a Notification is collapsed, and it is the first interaction
+            underTest.onNotificationExpansionChanged(
+                key = "key",
+                isExpanded = false,
+                location = ExpandableViewState.LOCATION_MAIN_AREA,
+                isUserAction = false
+            )
+            runCurrent()
+
+            // THEN no events are reported, because we consider the Notification initially
+            // collapsed, so only expanded is logged in the first time.
+            verifyZeroInteractions(mockStatusBarService)
+        }
+
+    @Test
+    fun onNotificationExpandedAndCollapsed_expansionChangesLogged() =
+        testScope.runTest {
+            // GIVEN a Notification is expanded
+            underTest.onNotificationExpansionChanged(
+                key = "key",
+                isExpanded = true,
+                location = ExpandableViewState.LOCATION_MAIN_AREA,
+                isUserAction = true
+            )
+            runCurrent()
+
+            // WHEN the Notification is collapsed
+            verify(mockStatusBarService)
+                .onNotificationExpansionChanged(
+                    /* key = */ "key",
+                    /* userAction = */ true,
+                    /* expanded = */ true,
+                    NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+                )
+
+            // AND the Notification is expanded again
+            underTest.onNotificationExpansionChanged(
+                key = "key",
+                isExpanded = false,
+                location = ExpandableViewState.LOCATION_MAIN_AREA,
+                isUserAction = true
+            )
+            runCurrent()
+
+            // THEN the expansion changes are logged
+            verify(mockStatusBarService)
+                .onNotificationExpansionChanged(
+                    /* key = */ "key",
+                    /* userAction = */ true,
+                    /* expanded = */ false,
+                    NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+                )
+        }
+
+    private fun fakeNotificationMaps(
+        vararg keys: String
+    ): Pair<Map<String, Int>, Map<String, Int>> {
+        val ranks: Map<String, Int> = keys.mapIndexed { index, key -> key to index }.toMap()
+        val locations: Map<String, Int> =
+            keys.associateWith { ExpandableViewState.LOCATION_MAIN_AREA }
+
+        return Pair(ranks, locations)
+    }
+
+    private fun assertThat(visibility: NotificationVisibility) =
+        NotificationVisibilitySubject(visibility)
+}
+
+private class NotificationVisibilitySubject(private val visibility: NotificationVisibility) {
+    fun isKeyEqualTo(key: String) = assertThat(visibility.key).isEqualTo(key)
+    fun isRankEqualTo(rank: Int) = assertThat(visibility.rank).isEqualTo(rank)
+    fun isCountEqualTo(count: Int) = assertThat(visibility.count).isEqualTo(count)
+    fun isVisible() = assertThat(this.visibility.visible).isTrue()
+    fun notVisible() = assertThat(this.visibility.visible).isFalse()
+    fun isInMainArea() =
+        assertThat(this.visibility.location)
+            .isEqualTo(NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index c17a8ef..4188c5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.runCurrent
 import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.dagger.NotificationStatsLoggerModule
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -71,6 +72,7 @@
                 FooterViewModelModule::class,
                 HeadlessSystemUserModeModule::class,
                 UnfoldTransitionModule.Bindings::class,
+                NotificationStatsLoggerModule::class,
             ]
     )
     interface TestComponent : SysUITestComponent<NotificationListViewModel> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt
new file mode 100644
index 0000000..e9d88cc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+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.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.data.repository.windowRootViewVisibilityRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationLoggerViewModelTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private val testScope = kosmos.testScope
+    private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
+    private val powerInteractor = kosmos.powerInteractor
+    private val windowRootViewVisibilityRepository = kosmos.windowRootViewVisibilityRepository
+
+    private val underTest = kosmos.notificationListLoggerViewModel
+
+    @Test
+    fun isLockscreenOrShadeInteractive_deviceActiveAndShadeIsInteractive_true() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(true)
+
+            val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive)
+
+            assertThat(actual).isTrue()
+        }
+
+    @Test
+    fun isLockscreenOrShadeInteractive_deviceIsAsleepAndShadeIsInteractive_false() =
+        testScope.runTest {
+            powerInteractor.setAsleepForTest()
+            windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(true)
+
+            val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive)
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun isLockscreenOrShadeInteractive_deviceActiveAndShadeIsNotInteractive_false() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(false)
+
+            val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive)
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun activeNotifications_hasNotifications() =
+        testScope.runTest {
+            activeNotificationListRepository.setActiveNotifs(5)
+
+            val notifs by collectLastValue(underTest.activeNotifications)
+
+            assertThat(notifs).hasSize(5)
+            requireNotNull(notifs).forEachIndexed { i, notif ->
+                assertThat(notif.key).isEqualTo("$i")
+            }
+        }
+
+    @Test
+    fun activeNotifications_isEmpty() =
+        testScope.runTest {
+            activeNotificationListRepository.setActiveNotifs(0)
+
+            val notifications by collectLastValue(underTest.activeNotifications)
+
+            assertThat(notifications).isEmpty()
+        }
+
+    @Test
+    fun activeNotificationRanks_hasNotifications() =
+        testScope.runTest {
+            val keys = (0..4).map { "$it" }
+            activeNotificationListRepository.setActiveNotifs(5)
+
+            val rankingsMap by collectLastValue(underTest.activeNotificationRanks)
+
+            assertThat(rankingsMap).hasSize(5)
+            keys.forEachIndexed { rank, key -> assertThat(rankingsMap).containsEntry(key, rank) }
+        }
+
+    @Test
+    fun activeNotificationRanks_isEmpty() =
+        testScope.runTest {
+            activeNotificationListRepository.setActiveNotifs(0)
+
+            val rankingsMap by collectLastValue(underTest.activeNotificationRanks)
+
+            assertThat(rankingsMap).isEmpty()
+        }
+
+    @Test
+    fun isOnLockScreen_true() =
+        testScope.runTest {
+            keyguardRepository.setKeyguardShowing(true)
+
+            val isOnLockScreen by collectLastValue(underTest.isOnLockScreen)
+
+            assertThat(isOnLockScreen).isTrue()
+        }
+    @Test
+    fun isOnLockScreen_false() =
+        testScope.runTest {
+            keyguardRepository.setKeyguardShowing(false)
+
+            val isOnLockScreen by collectLastValue(underTest.isOnLockScreen)
+
+            assertThat(isOnLockScreen).isFalse()
+        }
+}
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 20020f2..a824bc4 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
@@ -21,6 +21,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
@@ -39,8 +40,11 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.largeScreenHeaderHelper
+import com.android.systemui.shade.mockLargeScreenHeaderHelper
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
@@ -66,6 +70,7 @@
     val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     val shadeRepository = kosmos.shadeRepository
     val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor
+    val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper
 
     val underTest = kosmos.sharedNotificationContainerViewModel
 
@@ -101,8 +106,10 @@
         }
 
     @Test
-    fun validatePaddingTopInSplitShade() =
+    fun validatePaddingTopInSplitShade_refactorFlagOff_usesLargeHeaderResource() =
         testScope.runTest {
+            mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
             overrideResource(R.bool.config_use_split_notification_shade, true)
             overrideResource(R.dimen.large_screen_shade_header_height, 10)
             overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -115,6 +122,22 @@
         }
 
     @Test
+    fun validatePaddingTopInSplitShade_refactorFlagOn_usesLargeHeaderHelper() =
+        testScope.runTest {
+            mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
+            overrideResource(R.bool.config_use_split_notification_shade, true)
+            overrideResource(R.dimen.large_screen_shade_header_height, 10)
+            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
+
+            val dimens by collectLastValue(underTest.configurationBasedDimensions)
+
+            configurationRepository.onAnyConfigurationChange()
+
+            assertThat(dimens!!.paddingTop).isEqualTo(40)
+        }
+
+    @Test
     fun validatePaddingTop() =
         testScope.runTest {
             overrideResource(R.bool.config_use_split_notification_shade, false)
@@ -153,17 +176,41 @@
         }
 
     @Test
-    fun validateMarginTopWithLargeScreenHeader() =
+    fun validateMarginTopWithLargeScreenHeader_refactorFlagOff_usesResource() =
         testScope.runTest {
+            mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+            val headerResourceHeight = 50
+            val headerHelperHeight = 100
+            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+                .thenReturn(headerHelperHeight)
             overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, 50)
+            overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
             overrideResource(R.dimen.notification_panel_margin_top, 0)
 
             val dimens by collectLastValue(underTest.configurationBasedDimensions)
 
             configurationRepository.onAnyConfigurationChange()
 
-            assertThat(dimens!!.marginTop).isEqualTo(50)
+            assertThat(dimens!!.marginTop).isEqualTo(headerResourceHeight)
+        }
+
+    @Test
+    fun validateMarginTopWithLargeScreenHeader_refactorFlagOn_usesHelper() =
+        testScope.runTest {
+            mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+            val headerResourceHeight = 50
+            val headerHelperHeight = 100
+            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
+                .thenReturn(headerHelperHeight)
+            overrideResource(R.bool.config_use_large_screen_shade_header, true)
+            overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
+            overrideResource(R.dimen.notification_panel_margin_top, 0)
+
+            val dimens by collectLastValue(underTest.configurationBasedDimensions)
+
+            configurationRepository.onAnyConfigurationChange()
+
+            assertThat(dimens!!.marginTop).isEqualTo(headerHelperHeight)
         }
 
     @Test
@@ -275,11 +322,13 @@
         }
 
     @Test
-    fun boundsOnLockscreenInSplitShade() =
+    fun boundsOnLockscreenInSplitShade_refactorFlagOff_usesLargeHeaderResource() =
         testScope.runTest {
+            mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
             val bounds by collectLastValue(underTest.bounds)
 
             // When in split shade
+            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
             overrideResource(R.bool.config_use_split_notification_shade, true)
             overrideResource(R.dimen.large_screen_shade_header_height, 10)
             overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -300,6 +349,33 @@
         }
 
     @Test
+    fun boundsOnLockscreenInSplitShade_refactorFlagOn_usesLargeHeaderHelper() =
+        testScope.runTest {
+            mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+            val bounds by collectLastValue(underTest.bounds)
+
+            // When in split shade
+            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
+            overrideResource(R.bool.config_use_split_notification_shade, true)
+            overrideResource(R.dimen.large_screen_shade_header_height, 10)
+            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
+
+            configurationRepository.onAnyConfigurationChange()
+            runCurrent()
+
+            // Start on lockscreen
+            showLockscreen()
+
+            keyguardInteractor.setNotificationContainerBounds(
+                NotificationContainerBounds(top = 1f, bottom = 2f)
+            )
+            runCurrent()
+
+            // Top should be equal to bounds (1) + padding adjustment (40)
+            assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 41f, bottom = 2f))
+        }
+
+    @Test
     fun boundsOnShade() =
         testScope.runTest {
             val bounds by collectLastValue(underTest.bounds)
@@ -408,6 +484,46 @@
         }
 
     @Test
+    fun translationYUpdatesOnKeyguard() =
+        testScope.runTest {
+            val translationY by collectLastValue(underTest.translationY)
+
+            configurationRepository.setDimensionPixelSize(
+                R.dimen.keyguard_translate_distance_on_swipe_up,
+                -100
+            )
+            configurationRepository.onAnyConfigurationChange()
+
+            // legacy expansion means the user is swiping up, usually for the bouncer
+            shadeRepository.setLegacyShadeExpansion(0.5f)
+
+            showLockscreen()
+
+            // The translation values are negative
+            assertThat(translationY).isLessThan(0f)
+        }
+
+    @Test
+    fun translationYDoesNotUpdateWhenShadeIsExpanded() =
+        testScope.runTest {
+            val translationY by collectLastValue(underTest.translationY)
+
+            configurationRepository.setDimensionPixelSize(
+                R.dimen.keyguard_translate_distance_on_swipe_up,
+                -100
+            )
+            configurationRepository.onAnyConfigurationChange()
+
+            // legacy expansion means the user is swiping up, usually for the bouncer but also for
+            // shade collapsing
+            shadeRepository.setLegacyShadeExpansion(0.5f)
+
+            showLockscreenWithShadeExpanded()
+
+            assertThat(translationY).isEqualTo(0f)
+        }
+
+    @Test
     fun updateBounds_fromKeyguardRoot() =
         testScope.runTest {
             val bounds by collectLastValue(underTest.bounds)
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 4dc4798..bbf9a6b 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
@@ -30,11 +30,13 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.doze.util.BurnInHelperKt;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.core.FakeLogBuffer;
 import com.android.systemui.res.R;
+import com.android.systemui.shade.LargeScreenHeaderHelper;
 
 import org.junit.After;
 import org.junit.Before;
@@ -79,6 +81,7 @@
         MockitoAnnotations.initMocks(this);
         mStaticMockSession = mockitoSession()
                 .mockStatic(BurnInHelperKt.class)
+                .mockStatic(LargeScreenHeaderHelper.class)
                 .startMocking();
 
         LogBuffer logBuffer = FakeLogBuffer.Factory.Companion.create();
@@ -292,18 +295,44 @@
     }
 
     @Test
-    public void notifPaddingMakesUpToFullMarginInSplitShade() {
+    public void notifPaddingMakesUpToFullMarginInSplitShade_refactorFlagOff_usesResource() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR);
+        int keyguardSplitShadeTopMargin = 100;
+        int largeScreenHeaderHeightResource = 70;
         when(mResources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin))
-                .thenReturn(100);
+                .thenReturn(keyguardSplitShadeTopMargin);
         when(mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height))
-                .thenReturn(70);
+                .thenReturn(largeScreenHeaderHeightResource);
         mClockPositionAlgorithm.loadDimens(mContext, mResources);
         givenLockScreen();
         mIsSplitShade = true;
         // WHEN the position algorithm is run
         positionClock();
-        // THEN the notif padding makes up lacking margin (margin - header height = 30).
-        assertThat(mClockPosition.stackScrollerPadding).isEqualTo(30);
+        // THEN the notif padding makes up lacking margin (margin - header height).
+        int expectedPadding = keyguardSplitShadeTopMargin - largeScreenHeaderHeightResource;
+        assertThat(mClockPosition.stackScrollerPadding).isEqualTo(expectedPadding);
+    }
+
+    @Test
+    public void notifPaddingMakesUpToFullMarginInSplitShade_refactorFlagOn_usesHelper() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR);
+        int keyguardSplitShadeTopMargin = 100;
+        int largeScreenHeaderHeightHelper = 50;
+        int largeScreenHeaderHeightResource = 70;
+        when(LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext))
+                .thenReturn(largeScreenHeaderHeightHelper);
+        when(mResources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin))
+                .thenReturn(keyguardSplitShadeTopMargin);
+        when(mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height))
+                .thenReturn(largeScreenHeaderHeightResource);
+        mClockPositionAlgorithm.loadDimens(mContext, mResources);
+        givenLockScreen();
+        mIsSplitShade = true;
+        // WHEN the position algorithm is run
+        positionClock();
+        // THEN the notif padding makes up lacking margin (margin - header height).
+        int expectedPadding = keyguardSplitShadeTopMargin - largeScreenHeaderHeightHelper;
+        assertThat(mClockPosition.stackScrollerPadding).isEqualTo(expectedPadding);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 5102b4f..5fa7f13e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -166,7 +166,7 @@
                 mKeyguardRepository,
                 mCommandQueue,
                 PowerInteractorFactory.create().getPowerInteractor(),
-                mSceneTestUtils.getSceneContainerFlags(),
+                mSceneTestUtils.getFakeSceneContainerFlags(),
                 new FakeKeyguardBouncerRepository(),
                 new ConfigurationInteractor(new FakeConfigurationRepository()),
                 new FakeShadeRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 02e6fd5..7d91e8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -35,6 +35,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
@@ -87,6 +88,7 @@
                     Optional.empty(),
                     dispatcher,
                     testScope.backgroundScope,
+                    FakeLogBuffer.Factory.create(),
                     systemClock,
                 )
 
@@ -100,6 +102,22 @@
         }
 
     @Test
+    fun satelliteManagerThrows_doesNotCrash() =
+        testScope.runTest {
+            setupDefaultRepo()
+
+            whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any()))
+                .thenThrow(SatelliteException(13))
+
+            val conn by collectLastValue(underTest.connectionState)
+            val strength by collectLastValue(underTest.signalStrength)
+
+            // Flows have not emitted, we haven't crashed
+            assertThat(conn).isNull()
+            assertThat(strength).isNull()
+        }
+
+    @Test
     fun connectionState_mapsFromSatelliteModemState() =
         testScope.runTest {
             setupDefaultRepo()
@@ -380,6 +398,7 @@
                 if (satMan != null) Optional.of(satMan) else Optional.empty(),
                 dispatcher,
                 testScope.backgroundScope,
+                FakeLogBuffer.Factory.create(),
                 systemClock,
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
new file mode 100644
index 0000000..21c038a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class DeviceBasedSatelliteViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: DeviceBasedSatelliteViewModel
+    private lateinit var interactor: DeviceBasedSatelliteInteractor
+
+    private val repo = FakeDeviceBasedSatelliteRepository()
+    private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+    private val testScope = TestScope()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        interactor =
+            DeviceBasedSatelliteInteractor(
+                repo,
+                mobileIconsInteractor,
+                testScope.backgroundScope,
+            )
+
+        underTest =
+            DeviceBasedSatelliteViewModel(
+                interactor,
+                testScope.backgroundScope,
+            )
+    }
+
+    @Test
+    fun icon_nullWhenShouldNotShow_satelliteNotAllowed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is not allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = false
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+
+            // THEN icon is null because we should not be showing it
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun icon_nullWhenShouldNotShow_notAllOos() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are not OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = true
+
+            // THEN icon is null because we have service
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun icon_satelliteIsOff() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+
+            // THEN icon is null because we have service
+            assertThat(latest).isInstanceOf(Icon::class.java)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt
new file mode 100644
index 0000000..ca9df57
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.view
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.StatusBarIconView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Being a simple subclass of [ModernStatusBarView], use the same basic test cases to verify the
+ * root behavior, and add testing for the new [SingleBindableStatusBarIconView.withDefaultBinding]
+ * method.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class SingleBindableStatusBarIconViewTest : SysuiTestCase() {
+    private lateinit var binding: SingleBindableStatusBarIconViewBinding
+
+    // Visibility is outsourced to view-models. This simulates it
+    private var isVisible = true
+    private var visibilityFn: () -> Boolean = { isVisible }
+
+    @Test
+    fun initView_hasCorrectSlot() {
+        val view = createAndInitView()
+
+        assertThat(view.slot).isEqualTo(SLOT_NAME)
+    }
+
+    @Test
+    fun getVisibleState_icon_returnsIcon() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_ICON)
+    }
+
+    @Test
+    fun getVisibleState_dot_returnsDot() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_DOT)
+    }
+
+    @Test
+    fun getVisibleState_hidden_returnsHidden() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_HIDDEN)
+    }
+
+    @Test
+    fun onDarkChanged_bindingReceivesIconAndDecorTint() {
+        val view = createAndInitView()
+
+        view.onDarkChangedWithContrast(arrayListOf(), 0x12345678, 0x12344321)
+
+        assertThat(binding.iconTint).isEqualTo(0x12345678)
+        assertThat(binding.decorTint).isEqualTo(0x12345678)
+    }
+
+    @Test
+    fun setStaticDrawableColor_bindingReceivesIconTint() {
+        val view = createAndInitView()
+
+        view.setStaticDrawableColor(0x12345678, 0x12344321)
+
+        assertThat(binding.iconTint).isEqualTo(0x12345678)
+    }
+
+    @Test
+    fun setDecorColor_bindingReceivesDecorColor() {
+        val view = createAndInitView()
+
+        view.setDecorColor(0x23456789)
+
+        assertThat(binding.decorTint).isEqualTo(0x23456789)
+    }
+
+    @Test
+    fun isIconVisible_usesBinding_true() {
+        val view = createAndInitView()
+
+        isVisible = true
+
+        assertThat(view.isIconVisible).isEqualTo(true)
+    }
+
+    @Test
+    fun isIconVisible_usesBinding_false() {
+        val view = createAndInitView()
+
+        isVisible = false
+
+        assertThat(view.isIconVisible).isEqualTo(false)
+    }
+
+    @Test
+    fun getDrawingRect_takesTranslationIntoAccount() {
+        val view = createAndInitView()
+
+        view.translationX = 50f
+        view.translationY = 60f
+
+        val drawingRect = Rect()
+        view.getDrawingRect(drawingRect)
+
+        assertThat(drawingRect.left).isEqualTo(view.left + 50)
+        assertThat(drawingRect.right).isEqualTo(view.right + 50)
+        assertThat(drawingRect.top).isEqualTo(view.top + 60)
+        assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60)
+    }
+
+    private fun createAndInitView(): SingleBindableStatusBarIconView {
+        val view = SingleBindableStatusBarIconView.createView(context)
+        binding = SingleBindableStatusBarIconView.withDefaultBinding(view, visibilityFn) {}
+        view.initView(SLOT_NAME) { binding }
+        return view
+    }
+
+    companion object {
+        private const val SLOT_NAME = "test_slot"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index 89842d6..f63f79f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.collectLastValue
 import com.android.systemui.collectValues
+import com.android.systemui.communal.dagger.CommunalModule
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -58,6 +59,7 @@
         modules =
             [
                 SysUITestModule::class,
+                CommunalModule::class,
             ]
     )
     interface TestComponent : SysUITestComponent<CollapsedStatusBarViewModelImpl> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index 9419d63..5f9c096 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -55,7 +55,7 @@
             keyguardRepository,
             mock<CommandQueue>(),
             PowerInteractorFactory.create().powerInteractor,
-            sceneTestUtils.sceneContainerFlags,
+            sceneTestUtils.fakeSceneContainerFlags,
             FakeKeyguardBouncerRepository(),
             ConfigurationInteractor(FakeConfigurationRepository()),
             FakeShadeRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index fb5375a..e6b9d9b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -121,10 +121,10 @@
             SUPERVISED_USER_CREATION_APP_PACKAGE,
         )
 
-        utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+        utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
         mSetFlagsRule.enableFlags(AConfigFlags.FLAG_SWITCH_USER_ON_BG)
         spyContext = spy(context)
-        keyguardReply = KeyguardInteractorFactory.create(featureFlags = utils.featureFlags)
+        keyguardReply = KeyguardInteractorFactory.create(featureFlags = utils.fakeFeatureFlags)
         keyguardRepository = keyguardReply.repository
         userRepository = FakeUserRepository()
         refreshUsersScheduler =
@@ -363,7 +363,7 @@
     fun actions_deviceUnlocked_fullScreen() {
         createUserInteractor()
         testScope.runTest {
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
 
             userRepository.setUserInfos(userInfos)
@@ -447,7 +447,7 @@
     fun actions_deviceLockedAddFromLockscreenSet_fullList_fullScreen() {
         createUserInteractor()
         testScope.runTest {
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -792,7 +792,7 @@
     fun userRecordsFullScreen() {
         createUserInteractor()
         testScope.runTest {
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             userRepository.setUserInfos(userInfos)
@@ -901,7 +901,7 @@
     fun showUserSwitcher_fullScreenEnabled_launchesFullScreenDialog() {
         createUserInteractor()
         testScope.runTest {
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
 
             val expandable = mock<Expandable>()
             underTest.showUserSwitcher(expandable)
@@ -1139,7 +1139,7 @@
                         resetOrExitSessionReceiver = resetOrExitSessionReceiver,
                     ),
                 uiEventLogger = uiEventLogger,
-                featureFlags = utils.featureFlags,
+                featureFlags = utils.fakeFeatureFlags,
                 userRestrictionChecker = mock(),
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 30434c8..8920d4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -99,6 +99,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -111,6 +113,7 @@
 import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository;
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions;
 import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -439,6 +442,8 @@
                         () -> keyguardInteractor,
                         () -> mFromLockscreenTransitionInteractor,
                         () -> mFromPrimaryBouncerTransitionInteractor);
+        CommunalInteractor communalInteractor =
+                CommunalInteractorFactory.create().getCommunalInteractor();
 
         mFromLockscreenTransitionInteractor = new FromLockscreenTransitionInteractor(
                 keyguardTransitionRepository,
@@ -450,6 +455,12 @@
                 featureFlags,
                 shadeRepository,
                 powerInteractor,
+                new GlanceableHubTransitions(
+                        mTestScope,
+                        keyguardTransitionInteractor,
+                        keyguardTransitionRepository,
+                        communalInteractor
+                ),
                 () ->
                         new InWindowLauncherUnlockAnimationInteractor(
                                 new InWindowLauncherUnlockAnimationRepository(),
diff --git a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
index f96c508..5e254bf 100644
--- a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
@@ -16,9 +16,7 @@
 
 package android.content
 
-import com.android.systemui.SysuiTestableContext
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
 
-val Kosmos.testableContext: SysuiTestableContext by Kosmos.Fixture { testCase.context }
-var Kosmos.applicationContext: Context by Kosmos.Fixture { testableContext }
+var Kosmos.applicationContext: Context by Kosmos.Fixture { testCase.context }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt
similarity index 77%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
rename to packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt
index d98f496..bff0d0e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package android.service.notification
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.notificationListenerService by Fixture { mockNotificationListenerService }
+val Kosmos.mockNotificationListenerService by Fixture { mock<NotificationListenerService>() }
diff --git a/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.kt
new file mode 100644
index 0000000..a4ee702
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.telephony
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+
+val Kosmos.telephonyManager by Fixture {
+    mock<TelephonyManager> {
+        whenever(createForSubscriptionId(anyInt())).thenReturn(this)
+        whenever(supplyIccLockPin(anyString()))
+            .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 3))
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
index a11bf6a..d095f42 100644
--- a/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
@@ -18,6 +18,11 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.util.mockito.mock
 
 var Kosmos.accessibilityManager by Fixture { mock<AccessibilityManager>() }
+
+var Kosmos.accessibilityManagerWrapper by Fixture {
+    AccessibilityManagerWrapper(accessibilityManager)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt
similarity index 71%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt
index d98f496..fa3e8f9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.app
 
+import android.app.ActivityTaskManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.activityTaskManager by Fixture { mock<ActivityTaskManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
index a1815c5..22dff0a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
@@ -16,8 +16,12 @@
 
 package com.android.internal.logging
 
+import com.android.internal.logging.testing.FakeMetricsLogger
+import com.android.internal.util.LatencyTracker
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.metricsLogger by Fixture { mock<MetricsLogger>() }
+val Kosmos.fakeMetricsLogger by Fixture { FakeMetricsLogger() }
+val Kosmos.metricsLogger by Fixture<MetricsLogger> { fakeMetricsLogger }
+val Kosmos.latencyTracker by Fixture { mock<LatencyTracker>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt
similarity index 71%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt
index d98f496..3133437 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.internal.util
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.emergencyAffordanceManager by Fixture { mock<EmergencyAffordanceManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index d23dae9..af7f4c8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -39,6 +39,7 @@
 import androidx.test.uiautomator.UiDevice;
 
 import com.android.systemui.broadcast.FakeBroadcastDispatcher;
+import com.android.systemui.flags.SceneContainerRule;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -68,6 +69,9 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
+    @Rule(order = 10)
+    public final SceneContainerRule mSceneContainerRule = new SceneContainerRule();
+
     @Rule
     public SysuiTestableContext mContext = createTestableContext();
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index b28af46..b18859d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -18,13 +18,16 @@
 import android.app.ActivityManager
 import android.app.admin.DevicePolicyManager
 import android.os.UserManager
+import android.service.notification.NotificationListenerService
 import android.util.DisplayMetrics
 import android.view.LayoutInflater
 import com.android.internal.logging.MetricsLogger
+import com.android.internal.statusbar.IStatusBarService
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardViewController
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.ScreenLifecycle
@@ -48,9 +51,11 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
@@ -58,6 +63,7 @@
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.ZenModeController
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
@@ -79,6 +85,7 @@
     @get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(),
     @get:Provides val dozeParameters: DozeParameters = mock(),
     @get:Provides val dumpManager: DumpManager = mock(),
+    @get:Provides val headsUpManager: HeadsUpManager = mock(),
     @get:Provides val guestResumeSessionReceiver: GuestResumeSessionReceiver = mock(),
     @get:Provides val keyguardBypassController: KeyguardBypassController = mock(),
     @get:Provides val keyguardSecurityModel: KeyguardSecurityModel = mock(),
@@ -88,8 +95,10 @@
     val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock(),
     @get:Provides val mediaHierarchyManager: MediaHierarchyManager = mock(),
     @get:Provides val notifCollection: NotifCollection = mock(),
+    @get:Provides val notificationListLogger: NotificationStatsLogger = mock(),
     @get:Provides val notificationListener: NotificationListener = mock(),
     @get:Provides val notificationLockscreenUserManager: NotificationLockscreenUserManager = mock(),
+    @get:Provides val notificationPanelLogger: NotificationPanelLogger = mock(),
     @get:Provides val notificationMediaManager: NotificationMediaManager = mock(),
     @get:Provides val notificationShadeDepthController: NotificationShadeDepthController = mock(),
     @get:Provides
@@ -111,6 +120,7 @@
     @get:Provides val zenModeController: ZenModeController = mock(),
     @get:Provides val systemUIDialogManager: SystemUIDialogManager = mock(),
     @get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(),
+    @get:Provides val communalInteractor: CommunalInteractor = mock(),
 
     // log buffers
     @get:[Provides BroadcastDispatcherLog]
@@ -127,6 +137,10 @@
     @get:Provides val displayMetrics: DisplayMetrics = mock(),
     @get:Provides val metricsLogger: MetricsLogger = mock(),
     @get:Provides val userManager: UserManager = mock(),
+
+    // system server mocks
+    @get:Provides val mockStatusBarService: IStatusBarService = mock(),
+    @get:Provides val mockNotificationListenerService: NotificationListenerService = mock(),
 ) {
     @Module
     interface Bindings {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorKosmos.kt
new file mode 100644
index 0000000..4a089d3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorKosmos.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.display.data.repository.displayStateRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.util.mockito.mock
+import java.util.concurrent.Executor
+
+val Kosmos.displayStateInteractor by Fixture {
+    DisplayStateInteractorImpl(
+        applicationScope = applicationCoroutineScope,
+        context = applicationContext,
+        mainExecutor = mock<Executor>(),
+        displayStateRepository = displayStateRepository,
+        displayRepository = displayRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorKosmos.kt
new file mode 100644
index 0000000..e262066
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.biometrics.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.fingerprintPropertyInteractor by Fixture {
+    FingerprintPropertyInteractor(
+        context = applicationContext,
+        repository = fingerprintPropertyRepository,
+        configurationInteractor = configurationInteractor,
+        displayStateInteractor = displayStateInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
index d98f496..c0f8638 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.bouncer.data.repository
 
+import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.bouncerRepository by Fixture {
+    BouncerRepository(
+        flags = featureFlagsClassic,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt
new file mode 100644
index 0000000..8851709
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.repository
+
+import android.content.res.mainResources
+import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testScope
+
+val Kosmos.emergencyServicesRepository by Fixture {
+    EmergencyServicesRepository(
+        applicationScope = testScope.backgroundScope,
+        resources = mainResources,
+        configurationRepository = configurationRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
index ff5179a..8010261 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
@@ -2,6 +2,7 @@
 
 import com.android.systemui.biometrics.shared.SideFpsControllerRefactor
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
+import com.android.systemui.bouncer.shared.model.BouncerDismissActionModel
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.dagger.SysUISingleton
 import dagger.Binds
@@ -53,6 +54,7 @@
     override val alternateBouncerUIAvailable = _isAlternateBouncerUIAvailable.asStateFlow()
     private val _sideFpsShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
     override val sideFpsShowing: StateFlow<Boolean> = _sideFpsShowing.asStateFlow()
+    override var bouncerDismissActionModel: BouncerDismissActionModel? = null
 
     override fun setPrimaryScrimmed(isScrimmed: Boolean) {
         _primaryBouncerScrimmed.value = isScrimmed
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt
index d98f496..7af39df 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.bouncer.data.repository
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.fakeSimBouncerRepository by Fixture { FakeSimBouncerRepository() }
+
+val Kosmos.simBouncerRepository by Fixture<SimBouncerRepository> { fakeSimBouncerRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
index 86a4509..c4fc30d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.plugins.statusbar.statusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.time.fakeSystemClock
+import com.android.systemui.util.time.systemClock
 
 var Kosmos.alternateBouncerInteractor by
     Kosmos.Fixture {
@@ -35,7 +35,7 @@
             bouncerRepository = keyguardBouncerRepository,
             fingerprintPropertyRepository = fingerprintPropertyRepository,
             biometricSettingsRepository = biometricSettingsRepository,
-            systemClock = fakeSystemClock,
+            systemClock = systemClock,
             keyguardUpdateMonitor = keyguardUpdateMonitor,
             scope = testScope.backgroundScope,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt
new file mode 100644
index 0000000..5ced578
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import android.content.Intent
+import android.content.applicationContext
+import com.android.app.activityTaskManager
+import com.android.internal.logging.metricsLogger
+import com.android.internal.util.emergencyAffordanceManager
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.data.repository.emergencyServicesRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
+import com.android.systemui.telephony.domain.interactor.telephonyInteractor
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.mockito.mock
+import com.android.telecom.telecomManager
+
+val Kosmos.bouncerActionButtonInteractor by Fixture {
+    BouncerActionButtonInteractor(
+        applicationContext = applicationContext,
+        backgroundDispatcher = testDispatcher,
+        repository = emergencyServicesRepository,
+        mobileConnectionsRepository = mobileConnectionsRepository,
+        telephonyInteractor = telephonyInteractor,
+        authenticationInteractor = authenticationInteractor,
+        selectedUserInteractor = selectedUserInteractor,
+        activityTaskManager = activityTaskManager,
+        telecomManager = telecomManager,
+        emergencyAffordanceManager = emergencyAffordanceManager,
+        emergencyDialerIntentFactory =
+            object : EmergencyDialerIntentFactory {
+                override fun invoke(): Intent = Intent()
+            },
+        metricsLogger = metricsLogger,
+        dozeLogger = mock(),
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
new file mode 100644
index 0000000..27803b2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.bouncer.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.data.repository.bouncerRepository
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.bouncerInteractor by Fixture {
+    BouncerInteractor(
+        applicationScope = testScope.backgroundScope,
+        applicationContext = applicationContext,
+        repository = bouncerRepository,
+        authenticationInteractor = authenticationInteractor,
+        deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+        falsingInteractor = falsingInteractor,
+        powerInteractor = powerInteractor,
+        simBouncerInteractor = simBouncerInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
new file mode 100644
index 0000000..8ed9f45
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.bouncer.domain.interactor
+
+import android.content.Context
+import android.content.applicationContext
+import android.content.res.mainResources
+import android.telephony.euicc.EuiccManager
+import android.telephony.telephonyManager
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.bouncer.data.repository.simBouncerRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
+
+val Kosmos.simBouncerInteractor by Fixture {
+    SimBouncerInteractor(
+        applicationContext = applicationContext,
+        backgroundDispatcher = testDispatcher,
+        applicationScope = testScope.backgroundScope,
+        repository = simBouncerRepository,
+        telephonyManager = telephonyManager,
+        resources = mainResources,
+        keyguardUpdateMonitor = keyguardUpdateMonitor,
+        euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
+        mobileConnectionsRepository = mobileConnectionsRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
new file mode 100644
index 0000000..d91c597
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.flag.sceneContainerFlags
+import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.systemClock
+
+val Kosmos.bouncerViewModel by Fixture {
+    BouncerViewModel(
+        applicationContext = applicationContext,
+        applicationScope = testScope.backgroundScope,
+        mainDispatcher = testDispatcher,
+        bouncerInteractor = bouncerInteractor,
+        simBouncerInteractor = simBouncerInteractor,
+        authenticationInteractor = authenticationInteractor,
+        flags = sceneContainerFlags,
+        selectedUser = userSwitcherViewModel.selectedUser,
+        users = userSwitcherViewModel.users,
+        userSwitcherMenu = userSwitcherViewModel.menu,
+        actionButton = bouncerActionButtonInteractor.actionButton,
+        clock = systemClock,
+        devicePolicyManager = mock(),
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt
index d98f496..8fee5b2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.classifier.domain.interactor
 
+import com.android.systemui.classifier.falsingCollector
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.falsingInteractor by Fixture {
+    FalsingInteractor(
+        collector = falsingCollector,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt
similarity index 65%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt
index f9cdc1b..0768106 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.systemui.communal.data.repository
 
 import com.android.systemui.kosmos.Kosmos
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+val Kosmos.fakeCommunalMediaRepository by Kosmos.Fixture { FakeCommunalMediaRepository() }
+
+val Kosmos.communalMediaRepository by
+    Kosmos.Fixture<CommunalMediaRepository> { fakeCommunalMediaRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
index d98f496..1f5af5c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.communal.data.repository
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.fakeCommunalRepository by Fixture { FakeCommunalRepository() }
+
+val Kosmos.communalRepository by Fixture<CommunalRepository> { fakeCommunalRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt
similarity index 60%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt
index d98f496..c225e3c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.communal.data.repository
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.kosmos.applicationCoroutineScope
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.fakeCommunalWidgetRepository by Fixture {
+    FakeCommunalWidgetRepository(
+        coroutineScope = applicationCoroutineScope,
+    )
+}
+
+val Kosmos.communalWidgetRepository by
+    Fixture<CommunalWidgetRepository> { fakeCommunalWidgetRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
new file mode 100644
index 0000000..649b373
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalRepository
+import com.android.systemui.communal.data.repository.communalWidgetRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.smartspace.data.repository.smartspaceRepository
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.communalInteractor by Fixture {
+    CommunalInteractor(
+        communalRepository = communalRepository,
+        widgetRepository = communalWidgetRepository,
+        mediaRepository = communalMediaRepository,
+        smartspaceRepository = smartspaceRepository,
+        appWidgetHost = mock(),
+        keyguardInteractor = keyguardInteractor,
+        editWidgetsActivityStarter = mock(),
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
index 6bf527d..de58ae5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.util.time.fakeSystemClock
+import com.android.systemui.util.time.systemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 val Kosmos.deviceEntryHapticsInteractor by
@@ -37,7 +37,7 @@
             biometricSettingsRepository = biometricSettingsRepository,
             keyEventInteractor = keyEventInteractor,
             powerInteractor = powerInteractor,
-            systemClock = fakeSystemClock,
+            systemClock = systemClock,
             logger = biometricUnlockLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayRepositoryKosmos.kt
similarity index 72%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayRepositoryKosmos.kt
index f9cdc1b..048ea3c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.systemui.display.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+val Kosmos.displayRepository by Fixture { FakeDisplayRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayStateRepositoryKosmos.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayStateRepositoryKosmos.kt
index d98f496..4a71a09 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayStateRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.display.data.repository
 
+import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.displayStateRepository by Fixture { FakeDisplayStateRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
new file mode 100644
index 0000000..97f84c6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.flags
+
+import android.platform.test.annotations.EnableFlags
+import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+import com.android.systemui.Flags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL
+import com.android.systemui.Flags.FLAG_MEDIA_IN_SCENE_CONTAINER
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
+
+/**
+ * This includes @[EnableFlags] to work with [SetFlagsRule] to enable all aconfig flags required by
+ * that feature. It is also picked up by [SceneContainerRule] to set non-aconfig prerequisites.
+ */
+@EnableFlags(
+    FLAG_SCENE_CONTAINER,
+    FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
+    FLAG_KEYGUARD_SHADE_MIGRATION_NSSL,
+    FLAG_MEDIA_IN_SCENE_CONTAINER,
+)
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
+annotation class EnableSceneContainer
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
index abadaf7..7b36a29 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
@@ -30,7 +30,13 @@
  * Fixture supplying a shared [FakeFeatureFlagsClassic] instance. Can be accessed in tests in order
  * to override flag values.
  */
-val Kosmos.fakeFeatureFlagsClassic by Kosmos.Fixture { FakeFeatureFlagsClassic() }
+val Kosmos.fakeFeatureFlagsClassic by
+    Kosmos.Fixture {
+        FakeFeatureFlagsClassic().apply {
+            set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+            set(Flags.NSSL_DEBUG_LINES, false)
+        }
+    }
 
 /**
  * Fixture supplying a real [FeatureFlagsClassicRelease] instance, for use by tests that want to
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt
new file mode 100644
index 0000000..3faa6eb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags
+
+import android.util.Log
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Should always be used with [SetFlagsRule] and should be ordered after it.
+ *
+ * Used to ensure tests annotated with [EnableSceneContainer] can actually get `true` from
+ * [SceneContainerFlag.isEnabled].
+ */
+class SceneContainerRule : TestRule {
+    override fun apply(base: Statement?, description: Description?): Statement {
+        return object : Statement() {
+            @Throws(Throwable::class)
+            override fun evaluate() {
+                val initialEnabledValue = Flags.SCENE_CONTAINER_ENABLED
+                val hasAnnotation =
+                    description?.testClass?.getAnnotation(EnableSceneContainer::class.java) !=
+                        null || description?.getAnnotation(EnableSceneContainer::class.java) != null
+                if (hasAnnotation) {
+                    Assume.assumeTrue(
+                        "Compose must be available for @EnableSceneContainer test",
+                        ComposeFacade.isComposeAvailable()
+                    )
+                    Assume.assumeTrue(
+                        "Couldn't set Flags.SCENE_CONTAINER_ENABLED for @EnableSceneContainer test",
+                        trySetSceneContainerEnabled(true)
+                    )
+                    Assert.assertTrue(
+                        "SceneContainerFlag.isEnabled is false:" +
+                            "\n * Did you forget to add a new aconfig flag dependency in" +
+                            " @EnableSceneContainer?" +
+                            "\n * Did you forget to use SetFlagsRule with an earlier order?",
+                        SceneContainerFlag.isEnabled
+                    )
+                }
+                try {
+                    base?.evaluate()
+                } finally {
+                    if (hasAnnotation) {
+                        trySetSceneContainerEnabled(initialEnabledValue)
+                    }
+                }
+            }
+        }
+    }
+
+    companion object {
+        fun trySetSceneContainerEnabled(enabled: Boolean): Boolean {
+            if (Flags.SCENE_CONTAINER_ENABLED == enabled) {
+                return true
+            }
+            return try {
+                // TODO(b/283300105): remove this reflection setting once the hard-coded
+                //  Flags.SCENE_CONTAINER_ENABLED is no longer needed.
+                val field = Flags::class.java.getField("SCENE_CONTAINER_ENABLED")
+                field.isAccessible = true
+                field.set(null, enabled) // note: this does not work with multivalent tests
+                true
+            } catch (t: Throwable) {
+                Log.e("SceneContainerRule", "Unable to set SCENE_CONTAINER_ENABLED=$enabled", t)
+                false
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt
similarity index 71%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt
index d98f496..5c5016d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.jank
 
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.interactionJankMonitor by Fixture<InteractionJankMonitor> { mock() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
new file mode 100644
index 0000000..19cd950
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.data.repository
+
+import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
+import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.keyguardBlueprintRepository by
+    Kosmos.Fixture {
+        KeyguardBlueprintRepository(
+            configurationRepository = configurationRepository,
+            blueprints = setOf(defaultBlueprint),
+        )
+    }
+
+private val defaultBlueprint =
+    object : KeyguardBlueprint {
+        override val id: String
+            get() = DEFAULT
+        override val sections: List<KeyguardSection>
+            get() = listOf()
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt
new file mode 100644
index 0000000..59f0ec3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.domain.interactor
+
+import android.appwidget.AppWidgetHost
+import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalRepository
+import com.android.systemui.communal.data.repository.communalWidgetRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.smartspace.data.repository.smartspaceRepository
+import org.mockito.Mockito.mock
+
+val Kosmos.communalInteractor by
+    Kosmos.Fixture {
+        CommunalInteractor(
+            communalRepository = communalRepository,
+            widgetRepository = communalWidgetRepository,
+            mediaRepository = communalMediaRepository,
+            smartspaceRepository = smartspaceRepository,
+            keyguardInteractor = keyguardInteractor,
+            appWidgetHost = mock(AppWidgetHost::class.java),
+            editWidgetsActivityStarter = mock(EditWidgetsActivityStarter::class.java),
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index b1a0b67..3b38342 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -37,6 +37,7 @@
             flags = featureFlagsClassic,
             shadeRepository = shadeRepository,
             powerInteractor = powerInteractor,
+            glanceableHubTransitions = glanceableHubTransitions,
             inWindowLauncherUnlockAnimationInteractor =
                 Lazy { inWindowLauncherUnlockAnimationInteractor },
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
new file mode 100644
index 0000000..294b5ba
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.glanceableHubTransitions by
+    Kosmos.Fixture {
+        GlanceableHubTransitions(
+            scope = applicationCoroutineScope,
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            communalInteractor = communalInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
new file mode 100644
index 0000000..d9a3192
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.splitShadeStateController
+
+val Kosmos.keyguardBlueprintInteractor by
+    Kosmos.Fixture {
+        KeyguardBlueprintInteractor(
+            keyguardBlueprintRepository = keyguardBlueprintRepository,
+            applicationScope = applicationCoroutineScope,
+            context = applicationContext,
+            splitShadeStateController = splitShadeStateController,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
new file mode 100644
index 0000000..638a6a3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.domain.interactor
+
+import android.content.applicationContext
+import android.view.accessibility.accessibilityManagerWrapper
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.uiEventLogger
+
+val Kosmos.keyguardLongPressInteractor by
+    Kosmos.Fixture {
+        KeyguardLongPressInteractor(
+            appContext = applicationContext,
+            scope = applicationCoroutineScope,
+            transitionInteractor = keyguardTransitionInteractor,
+            repository = keyguardRepository,
+            logger = uiEventLogger,
+            featureFlags = featureFlagsClassic,
+            broadcastDispatcher = broadcastDispatcher,
+            accessibilityManager = accessibilityManagerWrapper,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..28fce77
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.glanceableHubToLockscreenTransitionViewModel by Fixture {
+    GlanceableHubToLockscreenTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
index d98f496..3c9846a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.keyguard.domain.interactor.keyguardLongPressInteractor
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.keyguardLongPressViewModel by
+    Kosmos.Fixture {
+        KeyguardLongPressViewModel(
+            interactor = keyguardLongPressInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 933f50c..5564767 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -39,5 +39,7 @@
         screenOffAnimationController = screenOffAnimationController,
         aodBurnInViewModel = aodBurnInViewModel,
         aodAlphaViewModel = aodAlphaViewModel,
+        lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
+        glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
new file mode 100644
index 0000000..96de4ba
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.biometrics.authController
+import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.lockscreenContentViewModel by
+    Kosmos.Fixture {
+        LockscreenContentViewModel(
+            clockInteractor = keyguardClockInteractor,
+            interactor = keyguardBlueprintInteractor,
+            authController = authController,
+            longPress = keyguardLongPressViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..9fe4ea3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.lockscreenToGlanceableHubTransitionViewModel by Fixture {
+    LockscreenToGlanceableHubTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index cac2646..73b7c50 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -16,7 +16,20 @@
 
 package com.android.systemui.plugins.statusbar
 
+import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.uiEventLogger
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.StatusBarStateControllerImpl
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.statusBarStateController by Kosmos.Fixture { mock<StatusBarStateController>() }
+var Kosmos.statusBarStateController by
+    Kosmos.Fixture {
+        StatusBarStateControllerImpl(
+            uiEventLogger,
+            interactionJankMonitor,
+            mock(),
+        ) {
+            shadeInteractor
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 09ab655..d314a25 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -16,182 +16,108 @@
 
 package com.android.systemui.scene
 
-import android.app.ActivityTaskManager
-import android.app.admin.DevicePolicyManager
 import android.content.Context
-import android.content.Intent
-import android.content.pm.UserInfo
-import android.graphics.Bitmap
-import android.graphics.drawable.BitmapDrawable
-import android.telecom.TelecomManager
-import android.telephony.PinResult
-import android.telephony.PinResult.PIN_RESULT_TYPE_SUCCESS
-import android.telephony.TelephonyManager
-import android.telephony.euicc.EuiccManager
-import com.android.internal.logging.MetricsLogger
-import com.android.internal.util.EmergencyAffordanceManager
+import android.content.applicationContext
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.repository.AuthenticationRepository
-import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.bouncer.data.repository.BouncerRepository
-import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.data.repository.bouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.EmergencyDialerIntentFactory
-import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel
 import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
-import com.android.systemui.common.shared.model.Text
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
-import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
-import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
-import com.android.systemui.doze.DozeLogger
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.data.repository.TrustRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.data.repository.PowerRepository
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.data.repository.SceneContainerRepository
+import com.android.systemui.scene.data.repository.sceneContainerRepository
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneKey
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
-import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
-import com.android.systemui.telephony.data.repository.TelephonyRepository
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
+import com.android.systemui.statusbar.phone.screenOffAnimationController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
-import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.telephony.domain.interactor.telephonyInteractor
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.user.ui.viewmodel.UserActionViewModel
-import com.android.systemui.user.ui.viewmodel.UserViewModel
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.CoroutineScope
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.time.systemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.currentTime
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyString
 
 /**
  * Utilities for creating scene container framework related repositories, interactors, and
  * view-models for tests.
  */
 @OptIn(ExperimentalCoroutinesApi::class)
-class SceneTestUtils(
-    private val context: Context,
-) {
-    constructor(test: SysuiTestCase) : this(context = test.context)
+@Deprecated("Please use Kosmos instead.")
+class SceneTestUtils {
 
     val kosmos = Kosmos()
-    val testDispatcher = kosmos.testDispatcher
-    val testScope = kosmos.testScope
-    val featureFlags =
-        FakeFeatureFlagsClassic().apply {
-            set(Flags.FULL_SCREEN_USER_SWITCHER, false)
-            set(Flags.NSSL_DEBUG_LINES, false)
-        }
-    val sceneContainerFlags = FakeSceneContainerFlags().apply { enabled = true }
-    val deviceEntryRepository: FakeDeviceEntryRepository by lazy { FakeDeviceEntryRepository() }
-    val authenticationRepository: FakeAuthenticationRepository by lazy {
-        FakeAuthenticationRepository(
-            currentTime = { testScope.currentTime },
-        )
-    }
-    val configurationRepository: FakeConfigurationRepository by lazy {
-        FakeConfigurationRepository()
-    }
-    val configurationInteractor: ConfigurationInteractor by lazy {
-        ConfigurationInteractor(configurationRepository)
-    }
-    private val emergencyServicesRepository: EmergencyServicesRepository by lazy {
-        EmergencyServicesRepository(
-            applicationScope = applicationScope(),
-            resources = context.resources,
-            configurationRepository = configurationRepository,
-        )
-    }
-    val telephonyRepository: FakeTelephonyRepository by lazy { FakeTelephonyRepository() }
-    val bouncerRepository = BouncerRepository(featureFlags)
-    val communalRepository: FakeCommunalRepository by lazy { FakeCommunalRepository() }
-    val keyguardRepository: FakeKeyguardRepository by lazy { FakeKeyguardRepository() }
-    val powerRepository: FakePowerRepository by lazy { FakePowerRepository() }
-    val simBouncerRepository: FakeSimBouncerRepository by lazy { FakeSimBouncerRepository() }
 
-    val clock: SystemClock = mock {
-        whenever(elapsedRealtime()).thenAnswer { testScope.currentTime }
-    }
-    val telephonyManager: TelephonyManager = mock {
-        whenever(createForSubscriptionId(anyInt())).thenReturn(this)
-        whenever(supplyIccLockPin(anyString())).thenReturn(PinResult(PIN_RESULT_TYPE_SUCCESS, 3))
-    }
-    val devicePolicyManager: DevicePolicyManager = mock {}
-    val mobileConnectionsRepository: FakeMobileConnectionsRepository by lazy {
-        FakeMobileConnectionsRepository(mock(), mock())
+    constructor(
+        context: Context,
+    ) {
+        kosmos.applicationContext = context
     }
 
-    val simBouncerInteractor =
-        SimBouncerInteractor(
-            applicationContext = context,
-            backgroundDispatcher = testDispatcher,
-            applicationScope = applicationScope(),
-            repository = simBouncerRepository,
-            telephonyManager = telephonyManager,
-            resources = context.resources,
-            keyguardUpdateMonitor = mock(),
-            euiccManager = context.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
-            mobileConnectionsRepository = mobileConnectionsRepository,
-        )
-
-    val userRepository: FakeUserRepository by lazy {
-        FakeUserRepository().apply {
-            val users = listOf(UserInfo(/* id=  */ 0, "name", /* flags= */ 0))
-            setUserInfos(users)
-            runBlocking { setSelectedUserInfo(users.first()) }
-        }
+    constructor(testCase: SysuiTestCase) : this(context = testCase.context) {
+        kosmos.testCase = testCase
     }
 
-    private val falsingCollectorFake: FalsingCollector by lazy { FalsingCollectorFake() }
-    private var falsingInteractor: FalsingInteractor? = null
-    private var powerInteractor: PowerInteractor? = null
+    val testDispatcher by lazy { kosmos.testDispatcher }
+    val testScope by lazy { kosmos.testScope }
+    val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic }
+    val fakeSceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags }
+    val deviceEntryRepository by lazy { kosmos.fakeDeviceEntryRepository }
+    val authenticationRepository by lazy { kosmos.fakeAuthenticationRepository }
+    val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
+    val configurationInteractor by lazy { kosmos.configurationInteractor }
+    val telephonyRepository by lazy { kosmos.fakeTelephonyRepository }
+    val bouncerRepository by lazy { kosmos.bouncerRepository }
+    val communalRepository by lazy { kosmos.fakeCommunalRepository }
+    val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+    val powerRepository by lazy { kosmos.fakePowerRepository }
+    val simBouncerRepository by lazy { kosmos.fakeSimBouncerRepository }
+    val clock by lazy { kosmos.systemClock }
+    val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository }
+    val simBouncerInteractor by lazy { kosmos.simBouncerInteractor }
+    val statusBarStateController by lazy { kosmos.statusBarStateController }
+    val interactionJankMonitor by lazy { kosmos.interactionJankMonitor }
+    val screenOffAnimationController by lazy { kosmos.screenOffAnimationController }
 
-    fun fakeSceneContainerRepository(
-        containerConfig: SceneContainerConfig = fakeSceneContainerConfig(),
-    ): SceneContainerRepository {
-        return SceneContainerRepository(applicationScope(), containerConfig)
+    fun fakeSceneContainerRepository(): SceneContainerRepository {
+        return kosmos.sceneContainerRepository
     }
 
     fun fakeSceneKeys(): List<SceneKey> {
@@ -202,222 +128,59 @@
         return kosmos.sceneContainerConfig
     }
 
-    @JvmOverloads
-    fun sceneInteractor(
-        repository: SceneContainerRepository = fakeSceneContainerRepository()
-    ): SceneInteractor {
-        return SceneInteractor(
-            applicationScope = applicationScope(),
-            repository = repository,
-            powerInteractor = powerInteractor(),
-            logger = mock(),
-        )
+    fun sceneInteractor(): SceneInteractor {
+        return kosmos.sceneInteractor
     }
 
-    fun deviceEntryInteractor(
-        repository: DeviceEntryRepository = deviceEntryRepository,
-        authenticationInteractor: AuthenticationInteractor,
-        sceneInteractor: SceneInteractor,
-        faceAuthRepository: DeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
-        trustRepository: TrustRepository = FakeTrustRepository(),
-    ): DeviceEntryInteractor {
-        return DeviceEntryInteractor(
-            applicationScope = applicationScope(),
-            repository = repository,
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-            deviceEntryFaceAuthRepository = faceAuthRepository,
-            trustRepository = trustRepository,
-            flags = FakeSceneContainerFlags(enabled = true)
-        )
+    fun deviceEntryInteractor(): DeviceEntryInteractor {
+        return kosmos.deviceEntryInteractor
     }
 
-    fun authenticationInteractor(
-        repository: AuthenticationRepository = authenticationRepository,
-    ): AuthenticationInteractor {
-        return AuthenticationInteractor(
-            applicationScope = applicationScope(),
-            repository = repository,
-            selectedUserInteractor = selectedUserInteractor(),
-        )
+    fun authenticationInteractor(): AuthenticationInteractor {
+        return kosmos.authenticationInteractor
     }
 
-    fun keyguardInteractor(
-        repository: KeyguardRepository = keyguardRepository
-    ): KeyguardInteractor {
-        return KeyguardInteractor(
-            repository = repository,
-            commandQueue = FakeCommandQueue(),
-            sceneContainerFlags = sceneContainerFlags,
-            bouncerRepository = FakeKeyguardBouncerRepository(),
-            configurationInteractor = configurationInteractor,
-            shadeRepository = FakeShadeRepository(),
-            sceneInteractorProvider = { sceneInteractor() },
-            powerInteractor = PowerInteractorFactory.create().powerInteractor,
-        )
+    fun keyguardInteractor(): KeyguardInteractor {
+        return kosmos.keyguardInteractor
     }
 
     fun communalInteractor(): CommunalInteractor {
-        return CommunalInteractorFactory.create(
-                communalRepository = communalRepository,
-            )
-            .communalInteractor
+        return kosmos.communalInteractor
     }
 
-    fun bouncerInteractor(
-        authenticationInteractor: AuthenticationInteractor,
-        deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor = mock(),
-    ): BouncerInteractor {
-        return BouncerInteractor(
-            applicationScope = applicationScope(),
-            applicationContext = context,
-            repository = bouncerRepository,
-            authenticationInteractor = authenticationInteractor,
-            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
-            falsingInteractor = falsingInteractor(),
-            powerInteractor = powerInteractor(),
-            simBouncerInteractor = simBouncerInteractor,
-        )
+    fun bouncerInteractor(): BouncerInteractor {
+        return kosmos.bouncerInteractor
     }
 
-    fun keyguardRootViewModel(): KeyguardRootViewModel = mock()
-
     fun notificationsPlaceholderViewModel(): NotificationsPlaceholderViewModel {
-        return NotificationsPlaceholderViewModel(
-            interactor =
-                NotificationStackAppearanceInteractor(
-                    repository = NotificationStackAppearanceRepository(),
-                ),
-            flags = sceneContainerFlags,
-            featureFlags = featureFlags,
-        )
+        return kosmos.notificationsPlaceholderViewModel
     }
 
-    fun bouncerViewModel(
-        bouncerInteractor: BouncerInteractor,
-        authenticationInteractor: AuthenticationInteractor,
-        actionButtonInteractor: BouncerActionButtonInteractor = bouncerActionButtonInteractor(),
-        users: List<UserViewModel> = createUsers(),
-    ): BouncerViewModel {
-        return BouncerViewModel(
-            applicationContext = context,
-            applicationScope = applicationScope(),
-            mainDispatcher = testDispatcher,
-            bouncerInteractor = bouncerInteractor,
-            simBouncerInteractor = simBouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            flags = sceneContainerFlags,
-            selectedUser = flowOf(users.first { it.isSelectionMarkerVisible }),
-            users = flowOf(users),
-            userSwitcherMenu = flowOf(createMenuActions()),
-            actionButton = actionButtonInteractor.actionButton,
-            clock = clock,
-            devicePolicyManager = devicePolicyManager,
-        )
+    fun bouncerViewModel(): BouncerViewModel {
+        return kosmos.bouncerViewModel
     }
 
-    fun telephonyInteractor(
-        repository: TelephonyRepository = telephonyRepository,
-    ): TelephonyInteractor {
-        return TelephonyInteractor(repository = repository)
+    fun telephonyInteractor(): TelephonyInteractor {
+        return kosmos.telephonyInteractor
     }
 
-    fun falsingInteractor(collector: FalsingCollector = falsingCollector()): FalsingInteractor {
-        return falsingInteractor ?: FalsingInteractor(collector).also { falsingInteractor = it }
+    fun falsingInteractor(): FalsingInteractor {
+        return kosmos.falsingInteractor
     }
 
     fun falsingCollector(): FalsingCollector {
-        return falsingCollectorFake
+        return kosmos.falsingCollector
     }
 
-    fun powerInteractor(
-        repository: PowerRepository = powerRepository,
-        falsingCollector: FalsingCollector = falsingCollector(),
-        screenOffAnimationController: ScreenOffAnimationController = mock(),
-        statusBarStateController: StatusBarStateController = mock(),
-    ): PowerInteractor {
-        return powerInteractor
-            ?: PowerInteractor(
-                    repository = repository,
-                    falsingCollector = falsingCollector,
-                    screenOffAnimationController = screenOffAnimationController,
-                    statusBarStateController = statusBarStateController,
-                )
-                .also { powerInteractor = it }
-    }
-
-    private fun applicationScope(): CoroutineScope {
-        return testScope.backgroundScope
-    }
-
-    private fun createUsers(
-        count: Int = 3,
-        selectedIndex: Int = 0,
-    ): List<UserViewModel> {
-        check(selectedIndex in 0 until count)
-
-        return buildList {
-            repeat(count) { index ->
-                add(
-                    UserViewModel(
-                        viewKey = index,
-                        name = Text.Loaded("name_$index"),
-                        image = BitmapDrawable(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)),
-                        isSelectionMarkerVisible = index == selectedIndex,
-                        alpha = 1f,
-                        onClicked = {},
-                    )
-                )
-            }
-        }
-    }
-
-    private fun createMenuActions(): List<UserActionViewModel> {
-        return buildList {
-            repeat(3) { index ->
-                add(
-                    UserActionViewModel(
-                        viewKey = index.toLong(),
-                        iconResourceId = 0,
-                        textResourceId = 0,
-                        onClicked = {},
-                    )
-                )
-            }
-        }
+    fun powerInteractor(): PowerInteractor {
+        return kosmos.powerInteractor
     }
 
     fun selectedUserInteractor(): SelectedUserInteractor {
-        return SelectedUserInteractor(userRepository)
+        return kosmos.selectedUserInteractor
     }
 
-    fun bouncerActionButtonInteractor(
-        mobileConnectionsRepository: MobileConnectionsRepository = mock(),
-        activityTaskManager: ActivityTaskManager = mock(),
-        telecomManager: TelecomManager? = null,
-        emergencyAffordanceManager: EmergencyAffordanceManager =
-            EmergencyAffordanceManager(context),
-        emergencyDialerIntentFactory: EmergencyDialerIntentFactory =
-            object : EmergencyDialerIntentFactory {
-                override fun invoke(): Intent = Intent()
-            },
-        metricsLogger: MetricsLogger = mock(),
-        dozeLogger: DozeLogger = mock(),
-    ): BouncerActionButtonInteractor {
-        return BouncerActionButtonInteractor(
-            applicationContext = context,
-            backgroundDispatcher = testDispatcher,
-            repository = emergencyServicesRepository,
-            mobileConnectionsRepository = mobileConnectionsRepository,
-            telephonyInteractor = telephonyInteractor(),
-            authenticationInteractor = authenticationInteractor(),
-            selectedUserInteractor = selectedUserInteractor(),
-            activityTaskManager = activityTaskManager,
-            telecomManager = telecomManager,
-            emergencyAffordanceManager = emergencyAffordanceManager,
-            emergencyDialerIntentFactory = emergencyDialerIntentFactory,
-            metricsLogger = metricsLogger,
-            dozeLogger = dozeLogger,
-        )
+    fun bouncerActionButtonInteractor(): BouncerActionButtonInteractor {
+        return kosmos.bouncerActionButtonInteractor
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt
index 7c4e160..e19941c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.scene.shared.model.sceneContainerConfig
+import com.android.systemui.scene.sceneContainerConfig
 
 val Kosmos.sceneContainerRepository by
     Kosmos.Fixture { SceneContainerRepository(applicationCoroutineScope, sceneContainerConfig) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt
index c2cdbed..979d8e7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt
@@ -18,4 +18,5 @@
 
 import com.android.systemui.kosmos.Kosmos
 
-var Kosmos.sceneContainerFlags by Kosmos.Fixture { FakeSceneContainerFlags() }
+var Kosmos.fakeSceneContainerFlags by Kosmos.Fixture { FakeSceneContainerFlags() }
+val Kosmos.sceneContainerFlags by Kosmos.Fixture<SceneContainerFlags> { fakeSceneContainerFlags }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
index b4fc948..8811b8d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
@@ -30,6 +30,7 @@
                     SceneKey.Lockscreen,
                     SceneKey.Bouncer,
                     SceneKey.Gone,
+                    SceneKey.Communal,
                 ),
             initialSceneKey = SceneKey.Lockscreen,
         ),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
index d98f496..0e4c923 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.smartspace.data.repository
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
+
+val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt
index 93a7adf..8385403 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar
 
-import android.content.testableContext
+import android.content.applicationContext
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +27,7 @@
 val Kosmos.lockscreenShadeScrimTransitionController by Fixture {
     LockscreenShadeScrimTransitionController(
         scrimController = scrimController,
-        context = testableContext,
+        context = applicationContext,
         configurationController = configurationController,
         dumpManager = dumpManager,
         splitShadeStateController = splitShadeStateController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt
index 2752cc2..1c6ce79 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar
 
-import android.content.testableContext
+import android.content.applicationContext
 import com.android.systemui.classifier.falsingCollector
 import com.android.systemui.classifier.falsingManager
 import com.android.systemui.dump.dumpManager
@@ -47,7 +47,7 @@
         scrimTransitionController = lockscreenShadeScrimTransitionController,
         keyguardTransitionControllerFactory = lockscreenShadeKeyguardTransitionControllerFactory,
         depthController = notificationShadeDepthController,
-        context = testableContext,
+        context = applicationContext,
         splitShadeOverScrollerFactory = splitShadeLockScreenOverScrollerFactory,
         singleShadeOverScrollerFactory = singleShadeLockScreenOverScrollerFactory,
         activityStarter = activityStarter,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifCollectionKosmos.kt
similarity index 77%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifCollectionKosmos.kt
index d98f496..9b27a9f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifCollectionKosmos.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.statusbar.notification.collection
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.notifCollection by Fixture { mockNotifCollection }
+val Kosmos.mockNotifCollection by Fixture { mock<NotifCollection>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
index 9851b0e..9c5c486 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.drawable.Icon
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN
 
 /** Simple ActiveNotificationModel builder for use in tests. */
 fun activeNotificationModel(
@@ -32,6 +33,11 @@
     aodIcon: Icon? = null,
     shelfIcon: Icon? = null,
     statusBarIcon: Icon? = null,
+    uid: Int = 0,
+    instanceId: Int? = null,
+    isGroupSummary: Boolean = false,
+    packageName: String = "pkg",
+    bucket: Int = BUCKET_UNKNOWN,
 ) =
     ActiveNotificationModel(
         key = key,
@@ -45,4 +51,9 @@
         aodIcon = aodIcon,
         shelfIcon = shelfIcon,
         statusBarIcon = statusBarIcon,
+        uid = uid,
+        packageName = packageName,
+        instanceId = instanceId,
+        isGroupSummary = isGroupSummary,
+        bucket = bucket,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
index cb1ba20..b40e1e7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
@@ -20,11 +20,20 @@
 
 /**
  * Make the repository hold [count] active notifications for testing. The keys of the notifications
- * are "0", "1", ..., (count - 1).toString().
+ * are "0", "1", ..., (count - 1).toString(). The ranks are the same values in Int.
  */
 fun ActiveNotificationListRepository.setActiveNotifs(count: Int) {
     this.activeNotifications.value =
         ActiveNotificationsStore.Builder()
-            .apply { repeat(count) { i -> addEntry(activeNotificationModel(key = i.toString())) } }
+            .apply {
+                val rankingsMap = mutableMapOf<String, Int>()
+                repeat(count) { i ->
+                    val key = "$i"
+                    addEntry(activeNotificationModel(key = key))
+                    rankingsMap[key] = i
+                }
+
+                setRankingsMap(rankingsMap)
+            }
             .build()
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
index 67fecb4..acc455f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
@@ -19,8 +19,8 @@
 import com.android.systemui.common.ui.configurationState
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.collection.notifCollection
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection
 import com.android.systemui.statusbar.ui.systemBarUtilsState
 
 val Kosmos.notificationIconContainerShelfViewBinder by Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt
similarity index 76%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt
index d98f496..30fc2f3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.statusbar.notification.logging
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.notificationPanelLogger by Fixture { mockNotificationPanelLogger }
+val Kosmos.mockNotificationPanelLogger by Fixture { mock<NotificationPanelLogger>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
index 83ac330..7f6f698 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack
 
-import android.content.testableContext
+import android.content.applicationContext
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +27,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 val Kosmos.ambientState by Fixture {
     AmbientState(
-        /*context=*/ testableContext,
+        /*context=*/ applicationContext,
         /*dumpManager=*/ dumpManager,
         /*sectionProvider=*/ stackScrollAlgorithmSectionProvider,
         /*bypassController=*/ stackScrollAlgorithmBypassController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt
similarity index 69%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt
index d98f496..b12f5af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.statusbar.notification.stack
 
+import com.android.internal.logging.latencyTracker
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.displaySwitchNotificationsHiderTracker by Fixture {
+    DisplaySwitchNotificationsHiderTracker(shadeInteractor, latencyTracker)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt
new file mode 100644
index 0000000..de52155
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import android.service.notification.notificationListenerService
+import com.android.internal.statusbar.statusBarService
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.logging.notificationPanelLogger
+
+val Kosmos.notificationStatsLogger by Fixture {
+    NotificationStatsLoggerImpl(
+        applicationScope = testScope,
+        bgDispatcher = testDispatcher,
+        statusBarService = statusBarService,
+        notificationListenerService = notificationListenerService,
+        notificationPanelLogger = notificationPanelLogger,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index 04716b9..748d04d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -23,8 +23,11 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger
+import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
 import com.android.systemui.statusbar.phone.notificationIconAreaController
+import java.util.Optional
 
 val Kosmos.notificationListViewBinder by Fixture {
     NotificationListViewBinder(
@@ -34,6 +37,8 @@
         falsingManager = falsingManager,
         iconAreaController = notificationIconAreaController,
         metricsLogger = metricsLogger,
+        hiderTracker = displaySwitchNotificationsHiderTracker,
         nicBinder = notificationIconContainerShelfViewBinder,
+        loggerOptional = Optional.of(notificationStatsLogger),
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt
similarity index 61%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt
index d98f496..08bda46 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt
@@ -14,11 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.notificationListLoggerViewModel by Fixture {
+    NotificationLoggerViewModel(
+        keyguardInteractor = keyguardInteractor,
+        windowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor,
+        activeNotificationsInteractor = activeNotificationsInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index f5a4c03..998e579 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -33,6 +33,7 @@
         shelf = notificationShelfViewModel,
         hideListViewModel = hideListViewModel,
         footer = Optional.of(footerViewModel),
+        logger = Optional.of(notificationListLoggerViewModel),
         activeNotificationsInteractor = activeNotificationsInteractor,
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         seenNotificationsInteractor = seenNotificationsInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
index e4313bb1..d9beabb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.data.repository.windowRootViewVisibilityRepository
 import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
@@ -27,7 +27,7 @@
 
 val Kosmos.windowRootViewVisibilityInteractor by Fixture {
     WindowRootViewVisibilityInteractor(
-        scope = testScope,
+        scope = applicationCoroutineScope,
         windowRootViewVisibilityRepository = windowRootViewVisibilityRepository,
         keyguardRepository = keyguardRepository,
         headsUpManager = headsUpManager,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt
similarity index 65%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt
index d98f496..9d62ea5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.fakeMobileConnectionsRepository by Fixture {
+    FakeMobileConnectionsRepository(tableLogBuffer = mock())
+}
+
+val Kosmos.mobileConnectionsRepository by
+    Fixture<MobileConnectionsRepository> { fakeMobileConnectionsRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelKosmos.kt
similarity index 60%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelKosmos.kt
index d98f496..0b9f897 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.systemui.user.ui.viewmodel
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.user.domain.interactor.guestUserInteractor
+import com.android.systemui.user.domain.interactor.userSwitcherInteractor
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+val Kosmos.userSwitcherViewModel by Fixture {
+    UserSwitcherViewModel(
+        userSwitcherInteractor = userSwitcherInteractor,
+        guestUserInteractor = guestUserInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
index 1f48d94..11c09ee 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
@@ -17,6 +17,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.util.time.FakeSystemClock
 import dagger.Binds
 import dagger.Module
@@ -27,8 +28,9 @@
 interface FakeExecutorModule {
     @Binds @Main @SysUISingleton fun bindMainExecutor(executor: FakeExecutor): Executor
 
+    @Binds @UiBackground @SysUISingleton fun bindUiBgExecutor(executor: FakeExecutor): Executor
+
     companion object {
-        @Provides
-        fun provideFake(clock: FakeSystemClock) = FakeExecutor(clock)
+        @Provides fun provideFake(clock: FakeSystemClock) = FakeExecutor(clock)
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
index 914e654..f3a8b14 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
@@ -17,5 +17,19 @@
 package com.android.systemui.util.time
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.currentTime
 
-var Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() }
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.systemClock by
+    Kosmos.Fixture<SystemClock> {
+        mock {
+            whenever(elapsedRealtime()).thenAnswer { testScope.currentTime }
+            whenever(uptimeMillis()).thenAnswer { testScope.currentTime }
+        }
+    }
+
+val Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt
similarity index 71%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt
index d98f496..4e0c0883 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+package com.android.telecom
 
+import android.telecom.TelecomManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.notifCollection by Fixture { mock<NotifCollection>() }
+var Kosmos.telecomManager by Fixture<TelecomManager?> { mock() }
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index cab2d74..5407af7 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -362,6 +362,7 @@
         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        packageFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
         packageFilter.addDataScheme("package");
         mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL,
                 packageFilter, null, mCallbackHandler);
@@ -402,6 +403,7 @@
         boolean added = false;
         boolean changed = false;
         boolean componentsModified = false;
+        int clearedUid = -1;
 
         final String pkgList[];
         switch (action) {
@@ -416,6 +418,10 @@
             case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
                 pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
                 break;
+            case Intent.ACTION_PACKAGE_DATA_CLEARED:
+                pkgList = null;
+                clearedUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+                break;
             default: {
                 Uri uri = intent.getData();
                 if (uri == null) {
@@ -430,7 +436,7 @@
                 changed = Intent.ACTION_PACKAGE_CHANGED.equals(action);
             }
         }
-        if (pkgList == null || pkgList.length == 0) {
+        if ((pkgList == null || pkgList.length == 0) && clearedUid == -1) {
             return;
         }
 
@@ -461,6 +467,8 @@
                         }
                     }
                 }
+            } else if (clearedUid != -1) {
+                componentsModified |= clearPreviewsForUidLocked(clearedUid);
             } else {
                 // If the package is being updated, we'll receive a PACKAGE_ADDED
                 // shortly, otherwise it is removed permanently.
@@ -486,6 +494,19 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private boolean clearPreviewsForUidLocked(int clearedUid) {
+        boolean changed = false;
+        final int providerCount = mProviders.size();
+        for (int i = 0; i < providerCount; i++) {
+            Provider provider = mProviders.get(i);
+            if (provider.id.uid == clearedUid) {
+                changed |= provider.clearGeneratedPreviewsLocked();
+            }
+        }
+        return changed;
+    }
+
     /**
      * Reload all widgets' masked state for the given user and its associated profiles, including
      * due to user not being available and package suspension.
@@ -3904,6 +3925,124 @@
         }
     }
 
+    @Override
+    @Nullable
+    public RemoteViews getWidgetPreview(@NonNull String callingPackage,
+            @NonNull ComponentName providerComponent, int profileId,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+        final int callingUserId = UserHandle.getCallingUserId();
+        if (DEBUG) {
+            Slog.i(TAG, "getWidgetPreview() " + callingUserId);
+        }
+        mSecurityPolicy.enforceCallFromPackage(callingPackage);
+        ensureWidgetCategoryCombinationIsValid(widgetCategory);
+
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(profileId);
+            final int providerCount = mProviders.size();
+            for (int i = 0; i < providerCount; i++) {
+                Provider provider = mProviders.get(i);
+                final ComponentName componentName = provider.id.componentName;
+                if (provider.zombie || !providerComponent.equals(componentName)) {
+                    continue;
+                }
+
+                final AppWidgetProviderInfo info = provider.getInfoLocked(mContext);
+                final int providerProfileId = info.getProfile().getIdentifier();
+                if (providerProfileId != profileId) {
+                    continue;
+                }
+
+                // Allow access to this provider if it is from the calling package or the caller has
+                // BIND_APPWIDGET permission.
+                final int callingUid = Binder.getCallingUid();
+                final String providerPackageName = componentName.getPackageName();
+                final boolean providerIsInCallerProfile =
+                        mSecurityPolicy.isProviderInCallerOrInProfileAndWhitelListed(
+                                providerPackageName, providerProfileId);
+                final boolean shouldFilterAppAccess = mPackageManagerInternal.filterAppAccess(
+                        providerPackageName, callingUid, providerProfileId);
+                final boolean providerIsInCallerPackage =
+                        mSecurityPolicy.isProviderInPackageForUid(provider, callingUid,
+                                callingPackage);
+                final boolean hasBindAppWidgetPermission =
+                        mSecurityPolicy.hasCallerBindPermissionOrBindWhiteListedLocked(
+                                callingPackage);
+                if (providerIsInCallerProfile && !shouldFilterAppAccess
+                        && (providerIsInCallerPackage || hasBindAppWidgetPermission)) {
+                    return provider.getGeneratedPreviewLocked(widgetCategory);
+                }
+            }
+        }
+        throw new IllegalArgumentException(
+                providerComponent + " is not a valid AppWidget provider");
+    }
+
+    @Override
+    public void setWidgetPreview(@NonNull ComponentName providerComponent,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+            @NonNull RemoteViews preview) {
+        final int userId = UserHandle.getCallingUserId();
+        if (DEBUG) {
+            Slog.i(TAG, "setWidgetPreview() " + userId);
+        }
+
+        // Make sure callers only set previews for their own package.
+        mSecurityPolicy.enforceCallFromPackage(providerComponent.getPackageName());
+
+        ensureWidgetCategoryCombinationIsValid(widgetCategories);
+
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(userId);
+
+            final ProviderId providerId = new ProviderId(Binder.getCallingUid(), providerComponent);
+            final Provider provider = lookupProviderLocked(providerId);
+            if (provider == null) {
+                throw new IllegalArgumentException(
+                        providerComponent + " is not a valid AppWidget provider");
+            }
+            provider.setGeneratedPreviewLocked(widgetCategories, preview);
+            scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+        }
+    }
+
+    @Override
+    public void removeWidgetPreview(@NonNull ComponentName providerComponent,
+            @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+        final int userId = UserHandle.getCallingUserId();
+        if (DEBUG) {
+            Slog.i(TAG, "removeWidgetPreview() " + userId);
+        }
+
+        // Make sure callers only remove previews for their own package.
+        mSecurityPolicy.enforceCallFromPackage(providerComponent.getPackageName());
+
+        ensureWidgetCategoryCombinationIsValid(widgetCategories);
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(userId);
+
+            final ProviderId providerId = new ProviderId(Binder.getCallingUid(), providerComponent);
+            final Provider provider = lookupProviderLocked(providerId);
+            if (provider == null) {
+                throw new IllegalArgumentException(
+                        providerComponent + " is not a valid AppWidget provider");
+            }
+            final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
+            if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+        }
+    }
+
+    private static void ensureWidgetCategoryCombinationIsValid(int widgetCategories) {
+        int validCategories = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+                | AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
+                | AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX;
+        int invalid = ~validCategories;
+        if ((widgetCategories & invalid) != 0) {
+            throw new IllegalArgumentException(widgetCategories
+                    + " is not a valid widget category combination");
+        }
+    }
+
     private final class CallbackHandler extends Handler {
         public static final int MSG_NOTIFY_UPDATE_APP_WIDGET = 1;
         public static final int MSG_NOTIFY_PROVIDER_CHANGED = 2;
@@ -4201,6 +4340,12 @@
         ArrayList<Widget> widgets = new ArrayList<>();
         PendingIntent broadcast;
         String infoTag;
+        SparseArray<RemoteViews> generatedPreviews = new SparseArray<>(3);
+        private static final int[] WIDGET_CATEGORY_FLAGS = new int[]{
+                AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN,
+                AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD,
+                AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX,
+        };
 
         boolean zombie; // if we're in safe mode, don't prune this just because nobody references it
 
@@ -4234,7 +4379,7 @@
             return false;
         }
 
-        @GuardedBy("AppWidgetServiceImpl.mLock")
+        @GuardedBy("this.mLock")
         public AppWidgetProviderInfo getInfoLocked(Context context) {
             if (!mInfoParsed) {
                 // parse
@@ -4250,6 +4395,7 @@
                     }
                     if (newInfo != null) {
                         info = newInfo;
+                        updateGeneratedPreviewCategoriesLocked();
                     }
                 }
                 mInfoParsed = true;
@@ -4279,6 +4425,62 @@
             mInfoParsed = true;
         }
 
+        @GuardedBy("this.mLock")
+        @Nullable
+        public RemoteViews getGeneratedPreviewLocked(
+                @AppWidgetProviderInfo.CategoryFlags int widgetCategories) {
+            for (int i = 0; i < generatedPreviews.size(); i++) {
+                if ((widgetCategories & generatedPreviews.keyAt(i)) != 0) {
+                    return generatedPreviews.valueAt(i);
+                }
+            }
+            return null;
+        }
+
+        @GuardedBy("this.mLock")
+        public void setGeneratedPreviewLocked(
+                @AppWidgetProviderInfo.CategoryFlags int widgetCategories,
+                @NonNull RemoteViews preview) {
+            for (int flag : WIDGET_CATEGORY_FLAGS) {
+                if ((widgetCategories & flag) != 0) {
+                    generatedPreviews.put(flag, preview);
+                }
+            }
+            updateGeneratedPreviewCategoriesLocked();
+        }
+
+        @GuardedBy("this.mLock")
+        public boolean removeGeneratedPreviewLocked(int widgetCategories) {
+            boolean changed = false;
+            for (int flag : WIDGET_CATEGORY_FLAGS) {
+                if ((widgetCategories & flag) != 0) {
+                    changed |= generatedPreviews.removeReturnOld(flag) != null;
+                }
+            }
+            if (changed) {
+                updateGeneratedPreviewCategoriesLocked();
+            }
+            return changed;
+        }
+
+        @GuardedBy("this.mLock")
+        public boolean clearGeneratedPreviewsLocked() {
+            if (generatedPreviews.size() > 0) {
+                generatedPreviews.clear();
+                updateGeneratedPreviewCategoriesLocked();
+                return true;
+            }
+            return false;
+        }
+
+        @GuardedBy("this.mLock")
+        private void updateGeneratedPreviewCategoriesLocked() {
+            info.generatedPreviewCategories = 0;
+            for (int i = 0; i < generatedPreviews.size(); i++) {
+                info.generatedPreviewCategories |= generatedPreviews.keyAt(i);
+            }
+        }
+
         @Override
         public String toString() {
             return "Provider{" + id + (zombie ? " Z" : "") + '}';
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6a81425..a856f42 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -4271,13 +4271,19 @@
                 if (value != null) {
                     viewState.setCurrentValue(value);
                 }
-
+                boolean isCredmanRequested = (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0;
                 if (shouldRequestSecondaryProvider(flags)) {
                     if (requestNewFillResponseOnViewEnteredIfNecessaryLocked(
                             id, viewState, flags)) {
                         Slog.v(TAG, "Started a new fill request for secondary provider.");
                         return;
                     }
+
+                    FillResponse response = viewState.getSecondaryResponse();
+                    if (response != null) {
+                        logPresentationStatsOnViewEntered(response, isCredmanRequested);
+                    }
+
                     // If the ViewState is ready to be displayed, onReady() will be called.
                     viewState.update(value, virtualBounds, flags);
 
@@ -4363,15 +4369,9 @@
                     return;
                 }
 
-                if (viewState.getResponse() != null) {
-                    boolean isCredmanRequested = (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0;
-                    FillResponse response = viewState.getResponse();
-                    mPresentationStatsEventLogger.maybeSetRequestId(response.getRequestId());
-                    mPresentationStatsEventLogger.maybeSetIsCredentialRequest(isCredmanRequested);
-                    mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId(
-                            mFieldClassificationIdSnapshot);
-                    mPresentationStatsEventLogger.maybeSetAvailableCount(
-                            response.getDatasets(), mCurrentViewId);
+                FillResponse response = viewState.getResponse();
+                if (response != null) {
+                    logPresentationStatsOnViewEntered(response, isCredmanRequested);
                 }
 
                 if (isSameViewEntered) {
@@ -4412,6 +4412,17 @@
     }
 
     @GuardedBy("mLock")
+    private void logPresentationStatsOnViewEntered(FillResponse response,
+            boolean isCredmanRequested) {
+        mPresentationStatsEventLogger.maybeSetRequestId(response.getRequestId());
+        mPresentationStatsEventLogger.maybeSetIsCredentialRequest(isCredmanRequested);
+        mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId(
+                mFieldClassificationIdSnapshot);
+        mPresentationStatsEventLogger.maybeSetAvailableCount(
+                response.getDatasets(), mCurrentViewId);
+    }
+
+    @GuardedBy("mLock")
     private void hideAugmentedAutofillLocked(@NonNull ViewState viewState) {
         if ((viewState.getState()
                 & ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL) != 0) {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 056ec89..50e1862 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -21,6 +21,7 @@
 import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES;
 import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
 import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
+import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION;
 import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
 import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
@@ -82,6 +83,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.UserInfo;
+import android.hardware.power.Mode;
 import android.net.MacAddress;
 import android.net.NetworkPolicyManager;
 import android.os.Binder;
@@ -90,6 +92,7 @@
 import android.os.Message;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
+import android.os.PowerManagerInternal;
 import android.os.PowerWhitelistManager;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -175,6 +178,7 @@
     private final PowerWhitelistManager mPowerWhitelistManager;
     private final UserManager mUserManager;
     final PackageManagerInternal mPackageManagerInternal;
+    private final PowerManagerInternal mPowerManagerInternal;
 
     /**
      * A structure that consists of two nested maps, and effectively maps (userId + packageName) to
@@ -235,6 +239,7 @@
 
         mOnPackageVisibilityChangeListener =
                 new OnPackageVisibilityChangeListener(mActivityManager);
+        mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
     }
 
     @Override
@@ -949,6 +954,10 @@
             mAssociationStore.updateAssociation(association);
 
             mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId);
+
+            if (association.getDeviceProfile() == REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION) {
+                mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true);
+            }
         }
 
         @Override
@@ -963,6 +972,10 @@
             }
 
             mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId);
+
+            if (association.getDeviceProfile() == REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION) {
+                mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
+            }
         }
 
         @Override
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index d1274d4..3b9d92d 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -30,6 +30,8 @@
 import android.hardware.input.VirtualMouseButtonEvent;
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusMotionEvent;
 import android.hardware.input.VirtualTouchEvent;
 import android.os.Handler;
 import android.os.IBinder;
@@ -71,12 +73,14 @@
     static final String PHYS_TYPE_MOUSE = "Mouse";
     static final String PHYS_TYPE_TOUCHSCREEN = "Touchscreen";
     static final String PHYS_TYPE_NAVIGATION_TOUCHPAD = "NavigationTouchpad";
+    static final String PHYS_TYPE_STYLUS = "Stylus";
     @StringDef(prefix = { "PHYS_TYPE_" }, value = {
             PHYS_TYPE_DPAD,
             PHYS_TYPE_KEYBOARD,
             PHYS_TYPE_MOUSE,
             PHYS_TYPE_TOUCHSCREEN,
             PHYS_TYPE_NAVIGATION_TOUCHPAD,
+            PHYS_TYPE_STYLUS,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface PhysType {
@@ -188,6 +192,16 @@
         }
     }
 
+    void createStylus(@NonNull String deviceName, int vendorId, int productId,
+            @NonNull IBinder deviceToken, int displayId, int height, int width)
+            throws DeviceCreationException {
+        final String phys = createPhys(PHYS_TYPE_STYLUS);
+        createDeviceInternal(InputDeviceDescriptor.TYPE_STYLUS, deviceName, vendorId,
+                productId, deviceToken, displayId, phys,
+                () -> mNativeWrapper.openUinputStylus(deviceName, vendorId, productId, phys,
+                        height, width));
+    }
+
     void unregisterInputDevice(@NonNull IBinder token) {
         synchronized (mLock) {
             final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove(
@@ -410,6 +424,32 @@
         }
     }
 
+    boolean sendStylusMotionEvent(@NonNull IBinder token, @NonNull VirtualStylusMotionEvent event) {
+        synchronized (mLock) {
+            final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
+                    token);
+            if (inputDeviceDescriptor == null) {
+                return false;
+            }
+            return mNativeWrapper.writeStylusMotionEvent(inputDeviceDescriptor.getNativePointer(),
+                    event.getToolType(), event.getAction(), event.getX(), event.getY(),
+                    event.getPressure(), event.getTiltX(), event.getTiltY(),
+                    event.getEventTimeNanos());
+        }
+    }
+
+    boolean sendStylusButtonEvent(@NonNull IBinder token, @NonNull VirtualStylusButtonEvent event) {
+        synchronized (mLock) {
+            final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
+                    token);
+            if (inputDeviceDescriptor == null) {
+                return false;
+            }
+            return mNativeWrapper.writeStylusButtonEvent(inputDeviceDescriptor.getNativePointer(),
+                    event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
+        }
+    }
+
     public void dump(@NonNull PrintWriter fout) {
         fout.println("    InputController: ");
         synchronized (mLock) {
@@ -437,7 +477,7 @@
         }
     }
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     Map<IBinder, InputDeviceDescriptor> getInputDeviceDescriptors() {
         final Map<IBinder, InputDeviceDescriptor> inputDeviceDescriptors = new ArrayMap<>();
         synchronized (mLock) {
@@ -454,6 +494,8 @@
             String phys);
     private static native long nativeOpenUinputTouchscreen(String deviceName, int vendorId,
             int productId, String phys, int height, int width);
+    private static native long nativeOpenUinputStylus(String deviceName, int vendorId,
+            int productId, String phys, int height, int width);
     private static native void nativeCloseUinput(long ptr);
     private static native boolean nativeWriteDpadKeyEvent(long ptr, int androidKeyCode, int action,
             long eventTimeNanos);
@@ -468,6 +510,10 @@
             float relativeY, long eventTimeNanos);
     private static native boolean nativeWriteScrollEvent(long ptr, float xAxisMovement,
             float yAxisMovement, long eventTimeNanos);
+    private static native boolean nativeWriteStylusMotionEvent(long ptr, int toolType, int action,
+            int locationX, int locationY, int pressure, int tiltX, int tiltY, long eventTimeNanos);
+    private static native boolean nativeWriteStylusButtonEvent(long ptr, int buttonCode, int action,
+            long eventTimeNanos);
 
     /** Wrapper around the static native methods for tests. */
     @VisibleForTesting
@@ -491,6 +537,11 @@
                     width);
         }
 
+        public long openUinputStylus(String deviceName, int vendorId, int productId, String phys,
+                int height, int width) {
+            return nativeOpenUinputStylus(deviceName, vendorId, productId, phys, height, width);
+        }
+
         public void closeUinput(long ptr) {
             nativeCloseUinput(ptr);
         }
@@ -527,21 +578,35 @@
                 long eventTimeNanos) {
             return nativeWriteScrollEvent(ptr, xAxisMovement, yAxisMovement, eventTimeNanos);
         }
+
+        public boolean writeStylusMotionEvent(long ptr, int toolType, int action, int locationX,
+                int locationY, int pressure, int tiltX, int tiltY, long eventTimeNanos) {
+            return nativeWriteStylusMotionEvent(ptr, toolType, action, locationX, locationY,
+                    pressure, tiltX, tiltY, eventTimeNanos);
+        }
+
+        public boolean writeStylusButtonEvent(long ptr, int buttonCode, int action,
+                long eventTimeNanos) {
+            return nativeWriteStylusButtonEvent(ptr, buttonCode, action, eventTimeNanos);
+        }
     }
 
-    @VisibleForTesting static final class InputDeviceDescriptor {
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    static final class InputDeviceDescriptor {
 
         static final int TYPE_KEYBOARD = 1;
         static final int TYPE_MOUSE = 2;
         static final int TYPE_TOUCHSCREEN = 3;
         static final int TYPE_DPAD = 4;
         static final int TYPE_NAVIGATION_TOUCHPAD = 5;
+        static final int TYPE_STYLUS = 6;
         @IntDef(prefix = { "TYPE_" }, value = {
                 TYPE_KEYBOARD,
                 TYPE_MOUSE,
                 TYPE_TOUCHSCREEN,
                 TYPE_DPAD,
                 TYPE_NAVIGATION_TOUCHPAD,
+                TYPE_STYLUS,
         })
         @Retention(RetentionPolicy.SOURCE)
         @interface Type {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 44c3a8d..f13f49a 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -23,6 +23,7 @@
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
 import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
@@ -75,6 +76,9 @@
 import android.hardware.input.VirtualMouseRelativeEvent;
 import android.hardware.input.VirtualMouseScrollEvent;
 import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusConfig;
+import android.hardware.input.VirtualStylusMotionEvent;
 import android.hardware.input.VirtualTouchEvent;
 import android.hardware.input.VirtualTouchscreenConfig;
 import android.os.Binder;
@@ -258,7 +262,9 @@
                 runningAppsChangedCallback,
                 params,
                 DisplayManagerGlobal.getInstance(),
-                Flags.virtualCamera() ? new VirtualCameraController() : null);
+                Flags.virtualCamera()
+                        ? new VirtualCameraController(params.getDevicePolicy(POLICY_TYPE_CAMERA))
+                        : null);
     }
 
     @VisibleForTesting
@@ -776,6 +782,26 @@
 
     @Override // Binder call
     @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void createVirtualStylus(@NonNull VirtualStylusConfig config,
+            @NonNull IBinder deviceToken) {
+        super.createVirtualStylus_enforcePermission();
+        Objects.requireNonNull(config);
+        Objects.requireNonNull(deviceToken);
+        checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mInputController.createStylus(config.getInputDeviceName(), config.getVendorId(),
+                    config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
+                    config.getHeight(), config.getWidth());
+        } catch (InputController.DeviceCreationException e) {
+            throw new IllegalArgumentException(e);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void unregisterInputDevice(IBinder token) {
         super.unregisterInputDevice_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
@@ -881,6 +907,36 @@
 
     @Override // Binder call
     @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public boolean sendStylusMotionEvent(@NonNull IBinder token,
+            @NonNull VirtualStylusMotionEvent event) {
+        super.sendStylusMotionEvent_enforcePermission();
+        Objects.requireNonNull(token);
+        Objects.requireNonNull(event);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendStylusMotionEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public boolean sendStylusButtonEvent(@NonNull IBinder token,
+            @NonNull VirtualStylusButtonEvent event) {
+        super.sendStylusButtonEvent_enforcePermission();
+        Objects.requireNonNull(token);
+        Objects.requireNonNull(event);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendStylusButtonEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void setShowPointerIcon(boolean showPointerIcon) {
         super.setShowPointerIcon_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
@@ -1337,6 +1393,11 @@
         }
     }
 
+    boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId) {
+        return mInputController.getInputDeviceDescriptors().values().stream().anyMatch(
+                inputDeviceDescriptor -> inputDeviceDescriptor.getInputDeviceId() == inputDeviceId);
+    }
+
     void onEnteringPipBlocked(int uid) {
         // Do nothing. ActivityRecord#checkEnterPictureInPictureState logs that the display does not
         // support PiP.
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 0d5cdcb..ef61498 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -838,10 +838,11 @@
         }
 
         @Override
-        public boolean isDisplayOwnedByAnyVirtualDevice(int displayId) {
+        public boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId) {
             ArrayList<VirtualDeviceImpl> virtualDevicesSnapshot = getVirtualDevicesSnapshot();
             for (int i = 0; i < virtualDevicesSnapshot.size(); i++) {
-                if (virtualDevicesSnapshot.get(i).isDisplayOwnedByVirtualDevice(displayId)) {
+                if (virtualDevicesSnapshot.get(i)
+                        .isInputDeviceOwnedByVirtualDevice(inputDeviceId)) {
                     return true;
                 }
             }
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
index 2f9b6a5..2d82b5e 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,10 +16,13 @@
 
 package com.android.server.companion.virtual.camera;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+
 import static com.android.server.companion.virtual.camera.VirtualCameraConversionUtil.getServiceCameraConfiguration;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.companion.virtual.VirtualDeviceParams.DevicePolicy;
 import android.companion.virtual.camera.VirtualCameraConfig;
 import android.companion.virtualcamera.IVirtualCameraService;
 import android.companion.virtualcamera.VirtualCameraConfiguration;
@@ -51,15 +54,21 @@
 
     @GuardedBy("mServiceLock")
     @Nullable private IVirtualCameraService mVirtualCameraService;
+    @DevicePolicy
+    private final int mCameraPolicy;
 
     @GuardedBy("mCameras")
     private final Map<IBinder, CameraDescriptor> mCameras = new ArrayMap<>();
 
-    public VirtualCameraController() {}
+    public VirtualCameraController(@DevicePolicy int cameraPolicy) {
+        this(/* virtualCameraService= */ null, cameraPolicy);
+    }
 
     @VisibleForTesting
-    VirtualCameraController(IVirtualCameraService virtualCameraService) {
+    VirtualCameraController(IVirtualCameraService virtualCameraService,
+            @DevicePolicy int cameraPolicy) {
         mVirtualCameraService = virtualCameraService;
+        mCameraPolicy = cameraPolicy;
     }
 
     /**
@@ -68,6 +77,8 @@
      * @param cameraConfig The {@link VirtualCameraConfig} sent by the client.
      */
     public void registerCamera(@NonNull VirtualCameraConfig cameraConfig) {
+        checkConfigByPolicy(cameraConfig);
+
         connectVirtualCameraServiceIfNeeded();
 
         try {
@@ -173,6 +184,29 @@
         }
     }
 
+    private void checkConfigByPolicy(VirtualCameraConfig config) {
+        if (mCameraPolicy == DEVICE_POLICY_DEFAULT) {
+            throw new IllegalArgumentException(
+                    "Cannot create virtual camera with DEVICE_POLICY_DEFAULT for "
+                            + "POLICY_TYPE_CAMERA");
+        } else if (isLensFacingAlreadyPresent(config.getLensFacing())) {
+            throw new IllegalArgumentException(
+                    "Only a single virtual camera can be created with lens facing "
+                            + config.getLensFacing());
+        }
+    }
+
+    private boolean isLensFacingAlreadyPresent(int lensFacing) {
+        synchronized (mCameras) {
+            for (CameraDescriptor cameraDescriptor : mCameras.values()) {
+                if (cameraDescriptor.mConfig.getLensFacing() == lensFacing) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private void connectVirtualCameraServiceIfNeeded() {
         synchronized (mServiceLock) {
             // Try to connect to service if not connected already.
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
index f24c4cc..c4a84b0 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -25,6 +25,7 @@
 import android.companion.virtualcamera.SupportedStreamConfiguration;
 import android.companion.virtualcamera.VirtualCameraConfiguration;
 import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
 import android.os.RemoteException;
 import android.view.Surface;
 
@@ -45,12 +46,12 @@
             getServiceCameraConfiguration(@NonNull VirtualCameraConfig cameraConfig)
                     throws RemoteException {
         VirtualCameraConfiguration serviceConfiguration = new VirtualCameraConfiguration();
-
         serviceConfiguration.supportedStreamConfigs =
                 cameraConfig.getStreamConfigs().stream()
                         .map(VirtualCameraConversionUtil::convertSupportedStreamConfiguration)
                         .toArray(SupportedStreamConfiguration[]::new);
-
+        serviceConfiguration.sensorOrientation = cameraConfig.getSensorOrientation();
+        serviceConfiguration.lensFacing = cameraConfig.getLensFacing();
         serviceConfiguration.virtualCameraCallback = convertCallback(cameraConfig.getCallback());
         return serviceConfiguration;
     }
@@ -60,12 +61,10 @@
             @NonNull IVirtualCameraCallback camera) {
         return new android.companion.virtualcamera.IVirtualCameraCallback.Stub() {
             @Override
-            public void onStreamConfigured(
-                    int streamId, Surface surface, int width, int height, int pixelFormat)
-                    throws RemoteException {
-                VirtualCameraStreamConfig streamConfig =
-                        createStreamConfig(width, height, pixelFormat);
-                camera.onStreamConfigured(streamId, surface, streamConfig);
+            public void onStreamConfigured(int streamId, Surface surface, int width, int height,
+                    int format) throws RemoteException {
+                camera.onStreamConfigured(streamId, surface, width, height,
+                        convertToJavaFormat(format));
             }
 
             @Override
@@ -81,23 +80,30 @@
     }
 
     @NonNull
-    private static VirtualCameraStreamConfig createStreamConfig(
-            int width, int height, int pixelFormat) {
-        return new VirtualCameraStreamConfig(width, height, pixelFormat);
-    }
-
-    @NonNull
     private static SupportedStreamConfiguration convertSupportedStreamConfiguration(
             VirtualCameraStreamConfig stream) {
         SupportedStreamConfiguration supportedConfig = new SupportedStreamConfiguration();
         supportedConfig.height = stream.getHeight();
         supportedConfig.width = stream.getWidth();
-        supportedConfig.pixelFormat = convertFormat(stream.getFormat());
+        supportedConfig.pixelFormat = convertToHalFormat(stream.getFormat());
+        supportedConfig.maxFps = stream.getMaximumFramesPerSecond();
         return supportedConfig;
     }
 
-    private static int convertFormat(int format) {
-        return format == ImageFormat.YUV_420_888 ? Format.YUV_420_888 : Format.UNKNOWN;
+    private static int convertToHalFormat(int javaFormat) {
+        return switch (javaFormat) {
+            case ImageFormat.YUV_420_888 -> Format.YUV_420_888;
+            case PixelFormat.RGBA_8888 -> Format.RGBA_8888;
+            default -> Format.UNKNOWN;
+        };
+    }
+
+    private static int convertToJavaFormat(int halFormat) {
+        return switch (halFormat) {
+            case Format.YUV_420_888 -> ImageFormat.YUV_420_888;
+            case Format.RGBA_8888 -> PixelFormat.RGBA_8888;
+            default -> ImageFormat.UNKNOWN;
+        };
     }
 
     private VirtualCameraConversionUtil() {
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 7a4ac6a..2b35231 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -5040,9 +5040,9 @@
 
         @Override
         public IFsveritySetupAuthToken createFsveritySetupAuthToken(ParcelFileDescriptor authFd,
-                int appUid, @UserIdInt int userId) throws IOException {
+                int uid) throws IOException {
             try {
-                return mInstaller.createFsveritySetupAuthToken(authFd, appUid, userId);
+                return mInstaller.createFsveritySetupAuthToken(authFd, uid);
             } catch (Installer.InstallerException e) {
                 throw new IOException(e);
             }
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index fd17261..c18bacb 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -172,6 +172,7 @@
     public static final String[] AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] {
             "android.hardware.audio.core.IModule/",
             "android.hardware.audio.core.IConfig/",
+            "android.hardware.audio.effect.IFactory/",
             "android.hardware.biometrics.face.IFace/",
             "android.hardware.biometrics.fingerprint.IFingerprint/",
             "android.hardware.bluetooth.IBluetoothHci/",
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0cff8b7..979449f 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -94,6 +94,8 @@
 import static android.os.Process.SHELL_UID;
 import static android.os.Process.SYSTEM_UID;
 import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
+import static android.content.flags.Flags.enableBindPackageIsolatedProcess;
+
 
 import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICE_BG_LAUNCH;
 import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_DELEGATE;
@@ -791,13 +793,15 @@
 
     static String getProcessNameForService(ServiceInfo sInfo, ComponentName name,
             String callingPackage, String instanceName, boolean isSdkSandbox,
-            boolean inSharedIsolatedProcess) {
+            boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess) {
         if (isSdkSandbox) {
             // For SDK sandbox, the process name is passed in as the instanceName
             return instanceName;
         }
-        if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
-            // For regular processes, just the name in sInfo
+        if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0
+                || (inPrivateSharedIsolatedProcess && !isDefaultProcessService(sInfo))) {
+            // For regular processes, or private package-shared isolated processes, just the name
+            // in sInfo
             return sInfo.processName;
         }
         // Isolated processes remain.
@@ -809,6 +813,10 @@
         }
     }
 
+    private static boolean isDefaultProcessService(ServiceInfo serviceInfo) {
+        return serviceInfo.applicationInfo.processName.equals(serviceInfo.processName);
+    }
+
     private static void traceInstant(@NonNull String message, @NonNull ServiceRecord service) {
         if (!Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
             return;
@@ -864,7 +872,7 @@
 
         ServiceLookupResult res = retrieveServiceLocked(service, instanceName, isSdkSandboxService,
                 sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage,
-                callingPid, callingUid, userId, true, callerFg, false, false, null, false);
+                callingPid, callingUid, userId, true, callerFg, false, false, null, false, false);
         if (res == null) {
             return null;
         }
@@ -1550,7 +1558,7 @@
         ServiceLookupResult r = retrieveServiceLocked(service, instanceName, isSdkSandboxService,
                 sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, null,
                 Binder.getCallingPid(), Binder.getCallingUid(), userId, false, false, false, false,
-                null, false);
+                null, false, false);
         if (r != null) {
             if (r.record != null) {
                 final long origId = Binder.clearCallingIdentity();
@@ -1642,7 +1650,7 @@
     IBinder peekServiceLocked(Intent service, String resolvedType, String callingPackage) {
         ServiceLookupResult r = retrieveServiceLocked(service, null, resolvedType, callingPackage,
                 Binder.getCallingPid(), Binder.getCallingUid(),
-                UserHandle.getCallingUserId(), false, false, false, false, false);
+                UserHandle.getCallingUserId(), false, false, false, false, false, false);
 
         IBinder ret = null;
         if (r != null) {
@@ -3714,6 +3722,9 @@
                 || (flags & Context.BIND_EXTERNAL_SERVICE_LONG) != 0;
         final boolean allowInstant = (flags & Context.BIND_ALLOW_INSTANT) != 0;
         final boolean inSharedIsolatedProcess = (flags & Context.BIND_SHARED_ISOLATED_PROCESS) != 0;
+        final boolean inPrivateSharedIsolatedProcess =
+                ((flags & Context.BIND_PACKAGE_ISOLATED_PROCESS) != 0)
+                        && enableBindPackageIsolatedProcess();
         final boolean matchQuarantined =
                 (flags & Context.BIND_MATCH_QUARANTINED_COMPONENTS) != 0;
 
@@ -3725,7 +3736,7 @@
                 isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage,
                 resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg,
                 isBindExternal, allowInstant, null /* fgsDelegateOptions */,
-                inSharedIsolatedProcess, matchQuarantined);
+                inSharedIsolatedProcess, inPrivateSharedIsolatedProcess, matchQuarantined);
         if (res == null) {
             return 0;
         }
@@ -4204,14 +4215,14 @@
     }
 
     private ServiceLookupResult retrieveServiceLocked(Intent service,
-            String instanceName, String resolvedType, String callingPackage,
-            int callingPid, int callingUid, int userId,
-            boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
-            boolean allowInstant, boolean inSharedIsolatedProcess) {
+            String instanceName, String resolvedType, String callingPackage, int callingPid,
+            int callingUid, int userId, boolean createIfNeeded, boolean callingFromFg,
+            boolean isBindExternal, boolean allowInstant, boolean inSharedIsolatedProcess,
+            boolean inPrivateSharedIsolatedProcess) {
         return retrieveServiceLocked(service, instanceName, false, INVALID_UID, null, resolvedType,
                 callingPackage, callingPid, callingUid, userId, createIfNeeded, callingFromFg,
                 isBindExternal, allowInstant, null /* fgsDelegateOptions */,
-                inSharedIsolatedProcess);
+                inSharedIsolatedProcess, inPrivateSharedIsolatedProcess);
     }
 
     // TODO(b/265746493): Special case for HotwordDetectionService,
@@ -4233,21 +4244,22 @@
             String callingPackage, int callingPid, int callingUid, int userId,
             boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
             boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions,
-            boolean inSharedIsolatedProcess) {
+            boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess) {
         return retrieveServiceLocked(service, instanceName, isSdkSandboxService,
                 sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage,
                 callingPid, callingUid, userId, createIfNeeded, callingFromFg, isBindExternal,
                 allowInstant, fgsDelegateOptions, inSharedIsolatedProcess,
-                false /* matchQuarantined */);
+                inPrivateSharedIsolatedProcess, false /* matchQuarantined */);
     }
 
-    private ServiceLookupResult retrieveServiceLocked(Intent service,
-            String instanceName, boolean isSdkSandboxService, int sdkSandboxClientAppUid,
-            String sdkSandboxClientAppPackage, String resolvedType,
+    private ServiceLookupResult retrieveServiceLocked(
+            Intent service, String instanceName, boolean isSdkSandboxService,
+            int sdkSandboxClientAppUid, String sdkSandboxClientAppPackage, String resolvedType,
             String callingPackage, int callingPid, int callingUid, int userId,
             boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
             boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions,
-            boolean inSharedIsolatedProcess, boolean matchQuarantined) {
+            boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess,
+            boolean matchQuarantined) {
         if (isSdkSandboxService && instanceName == null) {
             throw new IllegalArgumentException("No instanceName provided for sdk sandbox process");
         }
@@ -4344,7 +4356,8 @@
                 final ServiceRestarter res = new ServiceRestarter();
                 final String processName = getProcessNameForService(sInfo, cn, callingPackage,
                         null /* instanceName */, false /* isSdkSandbox */,
-                        false /* inSharedIsolatedProcess */);
+                        false /* inSharedIsolatedProcess */,
+                        false /*inPrivateSharedIsolatedProcess*/);
                 r = new ServiceRecord(mAm, cn /* name */, cn /* instanceName */,
                         sInfo.applicationInfo.packageName, sInfo.applicationInfo.uid, filter, sInfo,
                         callingFromFg, res, processName,
@@ -4415,6 +4428,10 @@
                             throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
                                     + className + " is not exported");
                         }
+                        if (inPrivateSharedIsolatedProcess) {
+                            throw new SecurityException("BIND_PACKAGE_ISOLATED_PROCESS cannot be "
+                                    + "applied to an external service.");
+                        }
                         if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
                             throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
                                     + className + " is not an isolatedProcess");
@@ -4448,28 +4465,32 @@
                     throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + name +
                             " is not an externalService");
                 }
-                if (inSharedIsolatedProcess) {
+                if (inSharedIsolatedProcess && inPrivateSharedIsolatedProcess) {
+                    throw new SecurityException("Either BIND_SHARED_ISOLATED_PROCESS or "
+                            + "BIND_PACKAGE_ISOLATED_PROCESS should be set. Not both.");
+                }
+                if (inSharedIsolatedProcess || inPrivateSharedIsolatedProcess) {
                     if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
                         throw new SecurityException("BIND_SHARED_ISOLATED_PROCESS failed, "
                                 + className + " is not an isolatedProcess");
                     }
+                }
+                if (inPrivateSharedIsolatedProcess && isDefaultProcessService(sInfo)) {
+                    throw new SecurityException("BIND_PACKAGE_ISOLATED_PROCESS cannot be used for "
+                            + "services running in the main app process.");
+                }
+                if (inSharedIsolatedProcess) {
+                    if (instanceName == null) {
+                        throw new IllegalArgumentException("instanceName must be provided for "
+                                + "binding a service into a shared isolated process.");
+                    }
                     if ((sInfo.flags & ServiceInfo.FLAG_ALLOW_SHARED_ISOLATED_PROCESS) == 0) {
                         throw new SecurityException("BIND_SHARED_ISOLATED_PROCESS failed, "
                                 + className + " has not set the allowSharedIsolatedProcess "
                                 + " attribute.");
                     }
-                    if (instanceName == null) {
-                        throw new IllegalArgumentException("instanceName must be provided for "
-                                + "binding a service into a shared isolated process.");
-                    }
                 }
                 if (userId > 0) {
-                    if (mAm.isSystemUserOnly(sInfo.flags)) {
-                        Slog.w(TAG_SERVICE, service + " is only available for the SYSTEM user,"
-                                + " calling userId is: " + userId);
-                        return null;
-                    }
-
                     if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo,
                             sInfo.name, sInfo.flags)
                             && mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) {
@@ -4503,11 +4524,13 @@
                             = new Intent.FilterComparison(service.cloneFilter());
                     final ServiceRestarter res = new ServiceRestarter();
                     String processName = getProcessNameForService(sInfo, name, callingPackage,
-                            instanceName, isSdkSandboxService, inSharedIsolatedProcess);
+                            instanceName, isSdkSandboxService, inSharedIsolatedProcess,
+                            inPrivateSharedIsolatedProcess);
                     r = new ServiceRecord(mAm, className, name, definingPackageName,
                             definingUid, filter, sInfo, callingFromFg, res,
                             processName, sdkSandboxClientAppUid,
-                            sdkSandboxClientAppPackage, inSharedIsolatedProcess);
+                            sdkSandboxClientAppPackage,
+                            (inSharedIsolatedProcess || inPrivateSharedIsolatedProcess));
                     res.setService(r);
                     smap.mServicesByInstanceName.put(name, r);
                     smap.mServicesByIntent.put(filter, r);
@@ -8504,7 +8527,8 @@
                 null /* sdkSandboxClientAppPackage */, null /* resolvedType */, callingPackage,
                 callingPid, callingUid, userId, true /* createIfNeeded */,
                 false /* callingFromFg */, false /* isBindExternal */, false /* allowInstant */ ,
-                options, false /* inSharedIsolatedProcess */);
+                options, false /* inSharedIsolatedProcess */,
+                false /*inPrivateSharedIsolatedProcess*/);
         if (res == null || res.record == null) {
             Slog.d(TAG,
                     "startForegroundServiceDelegateLocked retrieveServiceLocked returns null");
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e583a6c..f6d954a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4830,7 +4830,11 @@
             if (!mConstants.mEnableWaitForFinishAttachApplication) {
                 finishAttachApplicationInner(startSeq, callingUid, pid);
             }
-            maybeSendBootCompletedLocked(app);
+
+            // Temporarily disable sending BOOT_COMPLETED to see if this was impacting perf tests
+            if (false) {
+                maybeSendBootCompletedLocked(app);
+            }
         } catch (Exception e) {
             // We need kill the process group here. (b/148588589)
             Slog.wtf(TAG, "Exception thrown during bind of " + app, e);
@@ -6525,7 +6529,24 @@
     @Override
     public int checkUriPermission(Uri uri, int pid, int uid,
             final int modeFlags, int userId, IBinder callerToken) {
-        enforceNotIsolatedCaller("checkUriPermission");
+        return checkUriPermission(uri, pid, uid, modeFlags, userId,
+                /* isFullAccessForContentUri */ false, "checkUriPermission");
+    }
+
+    /**
+     * @param uri This uri must NOT contain an embedded userId.
+     * @param userId The userId in which the uri is to be resolved.
+     */
+    @Override
+    public int checkContentUriPermissionFull(Uri uri, int pid, int uid,
+            final int modeFlags, int userId) {
+        return checkUriPermission(uri, pid, uid, modeFlags, userId,
+                /* isFullAccessForContentUri */ true, "checkContentUriPermissionFull");
+    }
+
+    private int checkUriPermission(Uri uri, int pid, int uid,
+            final int modeFlags, int userId, boolean isFullAccessForContentUri, String methodName) {
+        enforceNotIsolatedCaller(methodName);
 
         // Our own process gets to do everything.
         if (pid == MY_PID) {
@@ -6536,8 +6557,10 @@
                 return PackageManager.PERMISSION_DENIED;
             }
         }
-        return mUgmInternal.checkUriPermission(new GrantUri(userId, uri, modeFlags), uid, modeFlags)
-                ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
+        boolean granted = mUgmInternal.checkUriPermission(new GrantUri(userId, uri, modeFlags), uid,
+                modeFlags, isFullAccessForContentUri);
+
+        return granted ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
     }
 
     @Override
@@ -9858,7 +9881,7 @@
 
 
     @Override
-    public void setApplicationStartInfoCompleteListener(
+    public void addApplicationStartInfoCompleteListener(
             IApplicationStartInfoCompleteListener listener, int userId) {
         enforceNotIsolatedCaller("setApplicationStartInfoCompleteListener");
 
@@ -9873,7 +9896,8 @@
 
 
     @Override
-    public void clearApplicationStartInfoCompleteListener(int userId) {
+    public void removeApplicationStartInfoCompleteListener(
+            IApplicationStartInfoCompleteListener listener, int userId) {
         enforceNotIsolatedCaller("clearApplicationStartInfoCompleteListener");
 
         // For the simplification, we don't support USER_ALL nor USER_CURRENT here.
@@ -9882,7 +9906,8 @@
         }
 
         final int callingUid = Binder.getCallingUid();
-        mProcessList.getAppStartInfoTracker().clearStartInfoCompleteListener(callingUid, true);
+        mProcessList.getAppStartInfoTracker().removeStartInfoCompleteListener(listener, callingUid,
+                true);
     }
 
     @Override
@@ -13747,11 +13772,6 @@
         return result;
     }
 
-    boolean isSystemUserOnly(int flags) {
-        return android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
-                && (flags & ServiceInfo.FLAG_SYSTEM_USER_ONLY) != 0;
-    }
-
     /**
      * Checks to see if the caller is in the same app as the singleton
      * component, or the component is in a special app. It allows special apps
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 82e554e..b90e5cd 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -119,7 +119,7 @@
 
     /** UID as key. */
     @GuardedBy("mLock")
-    private final SparseArray<ApplicationStartInfoCompleteCallback> mCallbacks;
+    private final SparseArray<ArrayList<ApplicationStartInfoCompleteCallback>> mCallbacks;
 
     /**
      * Whether or not we've loaded the historical app process start info from persistent storage.
@@ -465,11 +465,18 @@
         synchronized (mLock) {
             if (startInfo.getStartupState()
                     == ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN) {
-                ApplicationStartInfoCompleteCallback callback =
+                final List<ApplicationStartInfoCompleteCallback> callbacks =
                         mCallbacks.get(startInfo.getRealUid());
-                if (callback != null) {
-                    callback.onApplicationStartInfoComplete(startInfo);
+                if (callbacks == null) {
+                    return;
                 }
+                final int size = callbacks.size();
+                for (int i = 0; i < size; i++) {
+                    if (callbacks.get(i) != null) {
+                        callbacks.get(i).onApplicationStartInfoComplete(startInfo);
+                    }
+                }
+                mCallbacks.remove(startInfo.getRealUid());
             }
         }
     }
@@ -542,7 +549,6 @@
             } catch (RemoteException e) {
                 /*ignored*/
             }
-            clearStartInfoCompleteListener(mUid, true);
         }
 
         void unlinkToDeath() {
@@ -551,7 +557,7 @@
 
         @Override
         public void binderDied() {
-            clearStartInfoCompleteListener(mUid, false);
+            removeStartInfoCompleteListener(mCallback, mUid, false);
         }
     }
 
@@ -561,22 +567,43 @@
             if (!mEnabled) {
                 return;
             }
-            mCallbacks.put(uid, new ApplicationStartInfoCompleteCallback(listener, uid));
+            ArrayList<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(uid);
+            if (callbacks == null) {
+                mCallbacks.set(uid,
+                        callbacks = new ArrayList<ApplicationStartInfoCompleteCallback>());
+            }
+            callbacks.add(new ApplicationStartInfoCompleteCallback(listener, uid));
         }
     }
 
-    void clearStartInfoCompleteListener(final int uid, boolean unlinkDeathRecipient) {
+    void removeStartInfoCompleteListener(
+            final IApplicationStartInfoCompleteListener listener, final int uid,
+            boolean unlinkDeathRecipient) {
         synchronized (mLock) {
             if (!mEnabled) {
                 return;
             }
-            if (unlinkDeathRecipient) {
-                ApplicationStartInfoCompleteCallback callback = mCallbacks.get(uid);
-                if (callback != null) {
-                    callback.unlinkToDeath();
+            final ArrayList<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(uid);
+            if (callbacks == null) {
+                return;
+            }
+            final int size  = callbacks.size();
+            int index;
+            for (index = 0; index < size; index++) {
+                final ApplicationStartInfoCompleteCallback callback = callbacks.get(index);
+                if (callback.mCallback == listener) {
+                    if (unlinkDeathRecipient) {
+                        callback.unlinkToDeath();
+                    }
+                    break;
                 }
             }
-            mCallbacks.remove(uid);
+            if (index < size) {
+                callbacks.remove(index);
+            }
+            if (callbacks.isEmpty()) {
+                mCallbacks.remove(uid);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index 30f21a6..095d907 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -1249,9 +1249,9 @@
             ProviderInfo cpi = providers.get(i);
             boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo,
                     cpi.name, cpi.flags);
-            if (isSingletonOrSystemUserOnly(cpi) && app.userId != UserHandle.USER_SYSTEM) {
-                // This is a singleton or a SYSTEM user only provider, but a user besides the
-                // SYSTEM user is asking to initialize a process it runs
+            if (singleton && app.userId != UserHandle.USER_SYSTEM) {
+                // This is a singleton provider, but a user besides the
+                // default user is asking to initialize a process it runs
                 // in...  well, no, it doesn't actually run in this process,
                 // it runs in the process of the default user.  Get rid of it.
                 providers.remove(i);
@@ -1398,7 +1398,8 @@
                                     final boolean processMatch =
                                             Objects.equals(pi.processName, app.processName)
                                             || pi.multiprocess;
-                                    final boolean userMatch = !isSingletonOrSystemUserOnly(pi)
+                                    final boolean userMatch = !mService.isSingleton(
+                                            pi.processName, pi.applicationInfo, pi.name, pi.flags)
                                             || app.userId == UserHandle.USER_SYSTEM;
                                     final boolean isInstantApp = pi.applicationInfo.isInstantApp();
                                     final boolean splitInstalled = pi.splitName == null
@@ -1984,13 +1985,4 @@
             return isAuthRedirected;
         }
     }
-
-    /**
-     * Returns true if Provider is either singleUser or systemUserOnly provider.
-     */
-    private boolean isSingletonOrSystemUserOnly(ProviderInfo pi) {
-        return (android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
-                && mService.isSystemUserOnly(pi.flags))
-                || mService.isSingleton(pi.processName, pi.applicationInfo, pi.name, pi.flags);
-    }
 }
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index b03183c..fa5dbd2 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2935,7 +2935,11 @@
         return true;
     }
 
-    private static void freezeBinderAndPackageCgroup(ArrayList<Pair<ProcessRecord, Boolean>> procs,
+    private static boolean unfreezePackageCgroup(int packageUID) {
+        return freezePackageCgroup(packageUID, false);
+    }
+
+    private static void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs,
                                                      int packageUID) {
         // Freeze all binder processes under the target UID (whose cgroup is about to be frozen).
         // Since we're going to kill these, we don't need to unfreze them later.
@@ -2943,12 +2947,9 @@
         // processes (forks) should not be Binder users.
         int N = procs.size();
         for (int i = 0; i < N; i++) {
-            final int uid = procs.get(i).first.uid;
             final int pid = procs.get(i).first.getPid();
             int nRetries = 0;
-            // We only freeze the cgroup of the target package, so we do not need to freeze the
-            // Binder interfaces of dependant processes in other UIDs.
-            if (pid > 0 && uid == packageUID) {
+            if (pid > 0) {
                 try {
                     int rc;
                     do {
@@ -2962,12 +2963,19 @@
         }
 
         // We freeze the entire UID (parent) cgroup so that newly-specialized processes also freeze
-        // despite being added to a new child cgroup. The cgroups of package dependant processes are
-        // not frozen, since it's possible this would freeze processes with no dependency on the
-        // package being killed here.
+        // despite being added to a child cgroup created after this call that would otherwise be
+        // unfrozen.
         freezePackageCgroup(packageUID, true);
     }
 
+    private static List<Pair<ProcessRecord, Boolean>> getUIDSublist(
+            List<Pair<ProcessRecord, Boolean>> procs, int startIdx) {
+        final int uid = procs.get(startIdx).first.uid;
+        int endIdx = startIdx + 1;
+        while (endIdx < procs.size() && procs.get(endIdx).first.uid == uid) ++endIdx;
+        return procs.subList(startIdx, endIdx);
+    }
+
     @GuardedBy({"mService", "mProcLock"})
     boolean killPackageProcessesLSP(String packageName, int appId,
             int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart,
@@ -3063,25 +3071,36 @@
             }
         }
 
-        final int packageUID = UserHandle.getUid(userId, appId);
-        final boolean doFreeze = appId >= Process.FIRST_APPLICATION_UID
-                              && appId <= Process.LAST_APPLICATION_UID;
-        if (doFreeze) {
-            freezeBinderAndPackageCgroup(procs, packageUID);
+        final boolean killingUserApp = appId >= Process.FIRST_APPLICATION_UID
+                                    && appId <= Process.LAST_APPLICATION_UID;
+
+        if (killingUserApp) {
+            procs.sort((o1, o2) -> Integer.compare(o1.first.uid, o2.first.uid));
         }
 
-        int N = procs.size();
-        for (int i=0; i<N; i++) {
-            final Pair<ProcessRecord, Boolean> proc = procs.get(i);
-            removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second,
-                    reasonCode, subReason, reason, !doFreeze /* async */);
+        int idx = 0;
+        while (idx < procs.size()) {
+            final List<Pair<ProcessRecord, Boolean>> uidProcs = getUIDSublist(procs, idx);
+            final int packageUID = uidProcs.get(0).first.uid;
+
+            // Do not freeze for system apps or for dependencies of the targeted package, but
+            // make sure to freeze the targeted package for all users if called with USER_ALL.
+            final boolean doFreeze = killingUserApp && UserHandle.getAppId(packageUID) == appId;
+
+            if (doFreeze) freezeBinderAndPackageCgroup(uidProcs, packageUID);
+
+            for (Pair<ProcessRecord, Boolean> proc : uidProcs) {
+                removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second,
+                        reasonCode, subReason, reason, !doFreeze /* async */);
+            }
+            killAppZygotesLocked(packageName, appId, userId, false /* force */);
+
+            if (doFreeze) unfreezePackageCgroup(packageUID);
+
+            idx += uidProcs.size();
         }
-        killAppZygotesLocked(packageName, appId, userId, false /* force */);
         mService.updateOomAdjLocked(OOM_ADJ_REASON_PROCESS_END);
-        if (doFreeze) {
-            freezePackageCgroup(packageUID, false);
-        }
-        return N > 0;
+        return procs.size() > 0;
     }
 
     @GuardedBy("mService")
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index f80228a..99b45ec 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1812,22 +1812,21 @@
                                         "msg: MSG_L_SET_BT_ACTIVE_DEVICE "
                                             + "received with null profile proxy: "
                                             + btInfo)).printLog(TAG));
-                                sendMsg(MSG_CHECK_MUTE_MUSIC, SENDMSG_REPLACE, 0 /*delay*/);
-                                return;
-                            }
-                            @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
-                                    mBtHelper.getCodecWithFallback(btInfo.mDevice,
-                                            btInfo.mProfile, btInfo.mIsLeOutput,
-                                            "MSG_L_SET_BT_ACTIVE_DEVICE");
-                            mDeviceInventory.onSetBtActiveDevice(btInfo, codec,
-                                    (btInfo.mProfile
-                                            != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput)
-                                            ? mAudioService.getBluetoothContextualVolumeStream()
-                                            : AudioSystem.STREAM_DEFAULT);
-                            if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
-                                    || btInfo.mProfile == BluetoothProfile.HEARING_AID) {
-                                onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
-                                        "setBluetoothActiveDevice");
+                            } else {
+                                @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
+                                        mBtHelper.getCodecWithFallback(btInfo.mDevice,
+                                                btInfo.mProfile, btInfo.mIsLeOutput,
+                                                "MSG_L_SET_BT_ACTIVE_DEVICE");
+                                mDeviceInventory.onSetBtActiveDevice(btInfo, codec,
+                                        (btInfo.mProfile
+                                                != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput)
+                                                ? mAudioService.getBluetoothContextualVolumeStream()
+                                                : AudioSystem.STREAM_DEFAULT);
+                                if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
+                                        || btInfo.mProfile == BluetoothProfile.HEARING_AID) {
+                                    onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
+                                            "setBluetoothActiveDevice");
+                                }
                             }
                         }
                     }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index bf20ae3..57b19cd 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -764,7 +764,7 @@
     /** only public for mocking/spying, do not call outside of AudioService */
     // @GuardedBy("mDeviceBroker.mSetModeLock")
     @VisibleForTesting
-    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    //@GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
     public void onSetBtActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo btInfo,
                                     @AudioSystem.AudioFormatNativeEnumForBtCodec int codec,
                                     int streamType) {
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContext.java b/services/core/java/com/android/server/biometrics/log/BiometricContext.java
index 3dcea19..f8a9867 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricContext.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricContext.java
@@ -77,6 +77,9 @@
     @AuthenticateOptions.DisplayState
     int getDisplayState();
 
+    /** Gets whether touches on sensor are ignored by HAL */
+    boolean isHardwareIgnoringTouches();
+
     /**
      * Subscribe to context changes.
      *
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
index 95a047f..535b7b7 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
@@ -86,8 +86,8 @@
     @Nullable private final Handler mHandler;
     private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
     private int mFoldState = IBiometricContextListener.FoldState.UNKNOWN;
-
     private int mDisplayState = AuthenticateOptions.DISPLAY_STATE_UNKNOWN;
+    private boolean mIsHardwareIgnoringTouches = false;
     @VisibleForTesting
     final BroadcastReceiver mDockStateReceiver = new BroadcastReceiver() {
         @Override
@@ -129,6 +129,14 @@
                         notifyChanged();
                     }
                 }
+
+                @Override
+                public void onHardwareIgnoreTouchesChanged(boolean shouldIgnore) {
+                    if (mIsHardwareIgnoringTouches != shouldIgnore) {
+                        mIsHardwareIgnoringTouches = shouldIgnore;
+                        notifyChanged();
+                    }
+                }
             });
             service.registerSessionListener(SESSION_TYPES, new ISessionListener.Stub() {
                 @Override
@@ -215,6 +223,11 @@
     }
 
     @Override
+    public boolean isHardwareIgnoringTouches() {
+        return mIsHardwareIgnoringTouches;
+    }
+
+    @Override
     public void subscribe(@NonNull OperationContextExt context,
             @NonNull Consumer<OperationContext> consumer) {
         mSubscribers.put(context, consumer);
@@ -254,6 +267,7 @@
                 + "bp session: " + getBiometricPromptSessionInfo() + ", "
                 + "displayState: " + getDisplayState() + ", "
                 + "isAwake: " + isAwake() +  ", "
+                + "isHardwareIgnoring: " + isHardwareIgnoringTouches() +  ", "
                 + "isDisplayOn: " + isDisplayOn() +  ", "
                 + "dock: " + getDockedState() + ", "
                 + "rotation: " + getCurrentRotation() + ", "
diff --git a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java
index b4e0dff..0045d44 100644
--- a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java
+++ b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java
@@ -20,12 +20,14 @@
 import android.annotation.Nullable;
 import android.content.Intent;
 import android.hardware.biometrics.AuthenticateOptions;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.common.AuthenticateReason;
 import android.hardware.biometrics.common.DisplayState;
 import android.hardware.biometrics.common.FoldState;
 import android.hardware.biometrics.common.OperationContext;
 import android.hardware.biometrics.common.OperationReason;
+import android.hardware.biometrics.common.OperationState;
 import android.hardware.biometrics.common.WakeReason;
 import android.hardware.face.FaceAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
@@ -51,13 +53,26 @@
 
     /** Create a context. */
     public OperationContextExt(boolean isBP) {
-        this(new OperationContext(), isBP);
+        this(new OperationContext(), isBP, BiometricAuthenticator.TYPE_NONE);
+    }
+
+    public OperationContextExt(boolean isBP, @BiometricAuthenticator.Modality int modality) {
+        this(new OperationContext(), isBP, modality);
     }
 
     /** Create a wrapped context. */
-    public OperationContextExt(@NonNull OperationContext context, boolean isBP) {
+    public OperationContextExt(@NonNull OperationContext context, boolean isBP,
+            @BiometricAuthenticator.Modality int modality) {
         mAidlContext = context;
         mIsBP = isBP;
+
+        if (modality == BiometricAuthenticator.TYPE_FINGERPRINT) {
+            mAidlContext.operationState = OperationState.fingerprintOperationState(
+                    new OperationState.FingerprintOperationState());
+        } else if (modality == BiometricAuthenticator.TYPE_FACE) {
+            mAidlContext.operationState = OperationState.faceOperationState(
+                    new OperationState.FaceOperationState());
+        }
     }
 
     /**
@@ -247,12 +262,23 @@
         return mOrientation;
     }
 
+    /** The current operation state  */
+    public OperationState getOperationState() {
+        return mAidlContext.operationState;
+    }
+
     /** Update this object with the latest values from the given context. */
     OperationContextExt update(@NonNull BiometricContext biometricContext, boolean isCrypto) {
         mAidlContext.isAod = biometricContext.isAod();
         mAidlContext.displayState = toAidlDisplayState(biometricContext.getDisplayState());
         mAidlContext.foldState = toAidlFoldState(biometricContext.getFoldState());
         mAidlContext.isCrypto = isCrypto;
+
+        if (mAidlContext.operationState != null && mAidlContext.operationState.getTag()
+                == OperationState.fingerprintOperationState) {
+            mAidlContext.operationState.getFingerprintOperationState().isHardwareIgnoringTouches =
+                    biometricContext.isHardwareIgnoringTouches();
+        }
         setFirstSessionId(biometricContext);
 
         mIsDisplayOn = biometricContext.isDisplayOn();
diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java
index 1a682a9..1fc7c70 100644
--- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java
@@ -41,22 +41,42 @@
  * a common interface.
  */
 public class ClientMonitorCallbackConverter {
-    private IBiometricSensorReceiver mSensorReceiver; // BiometricService
-    private IFaceServiceReceiver mFaceServiceReceiver; // FaceManager
-    private IFingerprintServiceReceiver mFingerprintServiceReceiver; // FingerprintManager
+    private final IBiometricSensorReceiver mSensorReceiver; // BiometricService
+    private final IFaceServiceReceiver mFaceServiceReceiver; // FaceManager
+    private final IFingerprintServiceReceiver mFingerprintServiceReceiver; // FingerprintManager
 
     public ClientMonitorCallbackConverter(IBiometricSensorReceiver sensorReceiver) {
         mSensorReceiver = sensorReceiver;
+        mFaceServiceReceiver = null;
+        mFingerprintServiceReceiver = null;
     }
 
     public ClientMonitorCallbackConverter(IFaceServiceReceiver faceServiceReceiver) {
+        mSensorReceiver = null;
         mFaceServiceReceiver = faceServiceReceiver;
+        mFingerprintServiceReceiver = null;
     }
 
     public ClientMonitorCallbackConverter(IFingerprintServiceReceiver fingerprintServiceReceiver) {
+        mSensorReceiver = null;
+        mFaceServiceReceiver = null;
         mFingerprintServiceReceiver = fingerprintServiceReceiver;
     }
 
+    /**
+     * Returns an int representing the {@link BiometricAuthenticator.Modality} of the active
+     * ServiceReceiver
+     */
+    @BiometricAuthenticator.Modality
+    public int getModality() {
+        if (mFaceServiceReceiver != null) {
+            return BiometricAuthenticator.TYPE_FACE;
+        } else if (mFingerprintServiceReceiver != null) {
+            return BiometricAuthenticator.TYPE_FINGERPRINT;
+        }
+        return BiometricAuthenticator.TYPE_NONE;
+    }
+
     // The following apply to all clients
 
     public void onAcquired(int sensorId, int acquiredInfo, int vendorCode) throws RemoteException {
diff --git a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java
index 03658ce..0f01510 100644
--- a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.os.IBinder;
 
 import com.android.server.biometrics.log.BiometricContext;
@@ -58,7 +59,8 @@
         super(context, token, listener, userId, owner, cookie, sensorId,
                 biometricLogger, biometricContext);
         mLazyDaemon = lazyDaemon;
-        mOperationContext = new OperationContextExt(isBiometricPrompt());
+        int modality = listener != null ? listener.getModality() : BiometricAuthenticator.TYPE_NONE;
+        mOperationContext = new OperationContextExt(isBiometricPrompt(), modality);
     }
 
     @Nullable
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 29c5a3d..145885d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -28,6 +28,7 @@
 import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
 import android.hardware.biometrics.BiometricManager.Authenticators;
 import android.hardware.biometrics.common.ICancellationSignal;
+import android.hardware.biometrics.common.OperationState;
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintManager;
@@ -326,6 +327,12 @@
             if (session.hasContextMethods()) {
                 try {
                     session.getSession().onContextChanged(ctx);
+                    // TODO(b/317414324): Deprecate setIgnoreDisplayTouches
+                    if (ctx.operationState != null && ctx.operationState.getTag()
+                            == OperationState.fingerprintOperationState) {
+                        session.getSession().setIgnoreDisplayTouches(
+                                ctx.operationState.getFingerprintOperationState().isHardwareIgnoringTouches);
+                    }
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Unable to notify context changed", e);
                 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index e58e5ae..3aab7b3 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.hardware.biometrics.BiometricRequestConstants;
 import android.hardware.biometrics.common.ICancellationSignal;
+import android.hardware.biometrics.common.OperationState;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
@@ -122,6 +123,12 @@
             getBiometricContext().subscribe(opContext, ctx -> {
                 try {
                     session.getSession().onContextChanged(ctx);
+                    // TODO(b/317414324): Deprecate setIgnoreDisplayTouches
+                    if (ctx.operationState != null && ctx.operationState.getTag()
+                            == OperationState.fingerprintOperationState) {
+                        session.getSession().setIgnoreDisplayTouches(
+                                ctx.operationState.getFingerprintOperationState().isHardwareIgnoringTouches);
+                    }
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Unable to notify context changed", e);
                 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index c0761ed..bf5011d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -26,6 +26,7 @@
 import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
 import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.biometrics.common.ICancellationSignal;
+import android.hardware.biometrics.common.OperationState;
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintManager;
@@ -220,6 +221,12 @@
             getBiometricContext().subscribe(opContext, ctx -> {
                 try {
                     session.getSession().onContextChanged(ctx);
+                    // TODO(b/317414324): Deprecate setIgnoreDisplayTouches
+                    if (ctx.operationState != null && ctx.operationState.getTag()
+                            == OperationState.fingerprintOperationState) {
+                        session.getSession().setIgnoreDisplayTouches(
+                                ctx.operationState.getFingerprintOperationState().isHardwareIgnoringTouches);
+                    }
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Unable to notify context changed", e);
                 }
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 823788f..b179783 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -137,9 +137,9 @@
     public abstract boolean isAppRunningOnAnyVirtualDevice(int uid);
 
     /**
-     * Returns true if the {@code displayId} is owned by any virtual device
+     * @return whether the input device with the given id was created by a virtual device.
      */
-    public abstract boolean isDisplayOwnedByAnyVirtualDevice(int displayId);
+    public abstract boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId);
 
     /**
      * Gets the ids of VirtualDisplays owned by a VirtualDevice.
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 8910b6e..082776a 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -624,10 +624,10 @@
         pw.println("  Current mode="
                 + autoBrightnessModeToString(mCurrentBrightnessMapper.getMode()));
 
-        pw.println();
         for (int i = 0; i < mBrightnessMappingStrategyMap.size(); i++) {
+            pw.println();
             pw.println("  Mapper for mode "
-                    + autoBrightnessModeToString(mBrightnessMappingStrategyMap.keyAt(i)) + "=");
+                    + autoBrightnessModeToString(mBrightnessMappingStrategyMap.keyAt(i)) + ":");
             mBrightnessMappingStrategyMap.valueAt(i).dump(pw,
                     mBrightnessRangeController.getNormalBrightnessMax());
         }
@@ -1159,7 +1159,7 @@
         if (mCurrentBrightnessMapper.getMode() == mode) {
             return;
         }
-        Slog.i(TAG, "Switching to mode " + mode);
+        Slog.i(TAG, "Switching to mode " + autoBrightnessModeToString(mode));
         if (mode == AUTO_BRIGHTNESS_MODE_IDLE
                 || mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE) {
             switchModeAndShortTermModels(mode);
diff --git a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
index 544f490..e0bdda5 100644
--- a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
+++ b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
@@ -165,8 +165,15 @@
      */
     public float[] getLuxArray(@AutomaticBrightnessController.AutomaticBrightnessMode int mode,
             int preset) {
-        return mBrightnessLevelsLuxMap.get(
+        float[] luxArray = mBrightnessLevelsLuxMap.get(
                 autoBrightnessModeToString(mode) + "_" + autoBrightnessPresetToString(preset));
+        if (luxArray != null) {
+            return luxArray;
+        }
+
+        // No array for this preset, fall back to the normal preset
+        return mBrightnessLevelsLuxMap.get(autoBrightnessModeToString(mode) + "_"
+                + AutoBrightnessSettingName.normal.getRawName());
     }
 
     /**
@@ -184,8 +191,15 @@
      */
     public float[] getBrightnessArray(
             @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset) {
-        return mBrightnessLevelsMap.get(
+        float[] brightnessArray = mBrightnessLevelsMap.get(
                 autoBrightnessModeToString(mode) + "_" + autoBrightnessPresetToString(preset));
+        if (brightnessArray != null) {
+            return brightnessArray;
+        }
+
+        // No array for this preset, fall back to the normal preset
+        return mBrightnessLevelsMap.get(autoBrightnessModeToString(mode) + "_"
+                + AutoBrightnessSettingName.normal.getRawName());
     }
 
     @Override
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index be48eb4..1ae2559 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -100,6 +100,10 @@
             Flags.FLAG_ENABLE_VSYNC_LOW_POWER_VOTE,
             Flags::enableVsyncLowPowerVote);
 
+    private final FlagState mVsyncLowLightVote = new FlagState(
+            Flags.FLAG_ENABLE_VSYNC_LOW_LIGHT_VOTE,
+            Flags::enableVsyncLowLightVote);
+
     private final FlagState mBrightnessWearBedtimeModeClamperFlagState = new FlagState(
             Flags.FLAG_BRIGHTNESS_WEAR_BEDTIME_MODE_CLAMPER,
             Flags::brightnessWearBedtimeModeClamper);
@@ -220,6 +224,10 @@
         return mVsyncLowPowerVote.isEnabled();
     }
 
+    public boolean isVsyncLowLightVoteEnabled() {
+        return mVsyncLowLightVote.isEnabled();
+    }
+
     public boolean isBrightnessWearBedtimeModeClamperEnabled() {
         return mBrightnessWearBedtimeModeClamperFlagState.isEnabled();
     }
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index a2319a8..c2f52b5 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -146,6 +146,14 @@
 }
 
 flag {
+    name: "enable_vsync_low_light_vote"
+    namespace: "display_manager"
+    description: "Feature flag for vsync low light vote"
+    bug: "314921657"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "brightness_wear_bedtime_mode_clamper"
     namespace: "display_manager"
     description: "Feature flag for the Wear Bedtime mode brightness clamper"
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index ad3deff..8707000 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -215,7 +215,8 @@
         mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
         mSettingsObserver = new SettingsObserver(context, handler, mDvrrSupported,
                 displayManagerFlags);
-        mBrightnessObserver = new BrightnessObserver(context, handler, injector);
+        mBrightnessObserver = new BrightnessObserver(context, handler, injector, mDvrrSupported,
+                displayManagerFlags);
         mDefaultDisplayDeviceConfig = null;
         mUdfpsObserver = new UdfpsObserver();
         mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked,
@@ -1510,6 +1511,8 @@
         private final Injector mInjector;
         private final Handler mHandler;
 
+        private final boolean mVsyncLowLightBlockingVoteEnabled;
+
         private final IThermalEventListener.Stub mThermalListener =
                 new IThermalEventListener.Stub() {
                     @Override
@@ -1544,7 +1547,8 @@
         @GuardedBy("mLock")
         private @Temperature.ThrottlingStatus int mThermalStatus = Temperature.THROTTLING_NONE;
 
-        BrightnessObserver(Context context, Handler handler, Injector injector) {
+        BrightnessObserver(Context context, Handler handler, Injector injector,
+                boolean dvrrSupported , DisplayManagerFlags flags) {
             mContext = context;
             mHandler = handler;
             mInjector = injector;
@@ -1552,6 +1556,7 @@
                 /* attemptReadFromFeatureParams= */ false);
             mRefreshRateInHighZone = context.getResources().getInteger(
                     R.integer.config_fixedRefreshRateInHighZone);
+            mVsyncLowLightBlockingVoteEnabled = dvrrSupported && flags.isVsyncLowLightVoteEnabled();
         }
 
         /**
@@ -2131,7 +2136,17 @@
                                 Vote.forPhysicalRefreshRates(range.min, range.max);
                     }
                 }
-                refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
+
+                if (mVsyncLowLightBlockingVoteEnabled) {
+                    refreshRateSwitchingVote = Vote.forSupportedModesAndDisableRefreshRateSwitching(
+                            List.of(
+                                    new SupportedModesVote.SupportedMode(
+                                            /* peakRefreshRate= */ 60f, /* vsyncRate= */ 60f),
+                                    new SupportedModesVote.SupportedMode(
+                                            /* peakRefreshRate= */120f, /* vsyncRate= */ 120f)));
+                } else {
+                    refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
+                }
             }
 
             boolean insideHighZone = hasValidHighZone()
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
index 8f39570..e8d5a19 100644
--- a/services/core/java/com/android/server/display/mode/Vote.java
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -170,6 +170,13 @@
         return new SupportedModesVote(supportedModes);
     }
 
+
+    static Vote forSupportedModesAndDisableRefreshRateSwitching(
+            List<SupportedModesVote.SupportedMode> supportedModes) {
+        return new CombinedVote(
+                List.of(forDisableRefreshRateSwitching(), forSupportedModes(supportedModes)));
+    }
+
     static String priorityToString(int priority) {
         switch (priority) {
             case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
index a30c4d2..e80b9451 100644
--- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
+++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
@@ -16,12 +16,19 @@
 
 package com.android.server.display.mode;
 
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ACTIVE;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Trace;
 import android.util.SparseArray;
 import android.view.Display;
 
+import com.android.internal.util.FrameworkStatsLog;
+
 /**
  * The VotesStatsReporter is responsible for collecting and sending Vote related statistics 
  */
@@ -31,42 +38,77 @@
     private final boolean mIgnoredRenderRate;
     private final boolean mFrameworkStatsLogReportingEnabled;
 
+    private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1;
+
     public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) {
         mIgnoredRenderRate = ignoreRenderRate;
         mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled;
     }
 
-    void reportVoteAdded(int displayId, int priority,  @NonNull Vote vote) {
+    void reportVoteChanged(int displayId, int priority,  @Nullable Vote vote) {
+        if (vote == null) {
+            reportVoteRemoved(displayId, priority);
+        } else {
+            reportVoteAdded(displayId, priority, vote);
+        }
+    }
+
+    private void reportVoteAdded(int displayId, int priority,  @NonNull Vote vote) {
         int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
         Trace.traceCounter(Trace.TRACE_TAG_POWER,
                 TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate);
-        // if ( mFrameworkStatsLogReportingEnabled) {
-        // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, ADDED, maxRefreshRate, -1);
-        // }
+        if (mFrameworkStatsLogReportingEnabled) {
+            FrameworkStatsLog.write(
+                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
+                    maxRefreshRate, -1);
+        }
     }
 
-    void reportVoteRemoved(int displayId, int priority) {
+    private void reportVoteRemoved(int displayId, int priority) {
         Trace.traceCounter(Trace.TRACE_TAG_POWER,
                 TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1);
-        // if ( mFrameworkStatsLogReportingEnabled) {
-        // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, REMOVED, -1, -1);
-        // }
+        if (mFrameworkStatsLogReportingEnabled) {
+            FrameworkStatsLog.write(
+                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1);
+        }
     }
 
     void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode,
             SparseArray<Vote> votes) {
-//        if (!mFrameworkStatsLogReportingEnabled) {
-//            return;
-//        }
-//        int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1;
-//        for (int priority = minPriority; priority <= Vote.MAX_PRIORITY; priority ++) {
-//            Vote vote = votes.get(priority);
-//            if (vote != null) {
-//                int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
-//                FrameworkStatsLog.write(VOTE_CHANGED, displayId, priority,
-//                        ACTIVE, maxRefreshRate, selectedRefreshRate);
-//            }
-//        }
+        if (!mFrameworkStatsLogReportingEnabled) {
+            return;
+        }
+        int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1;
+        for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) {
+            if (priority < mLastMinPriorityReported && priority < minPriority) {
+                continue;
+            }
+            Vote vote = votes.get(priority);
+            if (vote == null) {
+                continue;
+            }
+
+            // Was previously reported ACTIVE, changed to ADDED
+            if (priority >= mLastMinPriorityReported && priority < minPriority) {
+                int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
+                FrameworkStatsLog.write(
+                        DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                        DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
+                        maxRefreshRate, selectedRefreshRate);
+            }
+            // Was previously reported ADDED, changed to ACTIVE
+            if (priority >= minPriority && priority < mLastMinPriorityReported) {
+                int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
+                FrameworkStatsLog.write(
+                        DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                        DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ACTIVE,
+                        maxRefreshRate, selectedRefreshRate);
+            }
+
+            mLastMinPriorityReported = minPriority;
+        }
     }
 
     private static int getMaxRefreshRate(@NonNull Vote vote, boolean ignoreRenderRate) {
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
index 7a1f7e9..56c7c18 100644
--- a/services/core/java/com/android/server/display/mode/VotesStorage.java
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -117,22 +117,13 @@
             Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes);
         }
         if (changed) {
-            reportVoteStats(displayId, priority, vote);
+            if (mVotesStatsReporter != null) {
+                mVotesStatsReporter.reportVoteChanged(displayId, priority, vote);
+            }
             mListener.onChanged();
         }
     }
 
-    private void reportVoteStats(int displayId, int priority, @Nullable Vote vote) {
-        if (mVotesStatsReporter == null) {
-            return;
-        }
-        if (vote == null) {
-            mVotesStatsReporter.reportVoteRemoved(displayId, priority);
-        } else {
-            mVotesStatsReporter.reportVoteAdded(displayId, priority, vote);
-        }
-    }
-
     /** dump class values, for debugging */
     void dump(@NonNull PrintWriter pw) {
         SparseArray<SparseArray<Vote>> votesByDisplayLocal = new SparseArray<>();
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 1cd267d..d34661d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1290,15 +1290,19 @@
             mService.getHdmiCecNetwork().removeCecSwitches(portId);
         }
 
-        // Turning System Audio Mode off when the AVR is unlugged or standby.
-        // When the device is not unplugged but reawaken from standby, we check if the System
-        // Audio Control Feature is enabled or not then decide if turning SAM on/off accordingly.
-        if (getAvrDeviceInfo() != null && portId == getAvrDeviceInfo().getPortId()) {
-            HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected);
-            if (!connected) {
-                setSystemAudioMode(false);
-            } else {
-                onNewAvrAdded(getAvrDeviceInfo());
+        if (!mService.isEarcEnabled() || !mService.isEarcSupported()) {
+            HdmiDeviceInfo avr = getAvrDeviceInfo();
+            if (avr != null
+                    && portId == avr.getPortId()
+                    && isConnectedToArcPort(avr.getPhysicalAddress())) {
+                HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected);
+                if (connected) {
+                    if (mArcEstablished) {
+                        enableAudioReturnChannel(true);
+                    }
+                } else {
+                    enableAudioReturnChannel(false);
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index eaf754d..e0e825d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -3617,7 +3617,7 @@
         }
     }
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     protected boolean isEarcSupported() {
         synchronized (mLock) {
             return mEarcSupported;
diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
index f3532e5..b6c0e5d 100644
--- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java
+++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
@@ -53,6 +53,7 @@
     private int mVendorId;
     private String mDisplayName;
     private int mTimeoutRetry;
+    private HdmiDeviceInfo mOldDeviceInfo;
 
     /**
      * Constructor.
@@ -73,6 +74,38 @@
 
     @Override
     public boolean start() {
+        mOldDeviceInfo =
+            localDevice().mService.getHdmiCecNetwork().getCecDeviceInfo(mDeviceLogicalAddress);
+        // If there's deviceInfo with same (logical address, physical address) set
+        // Then addCecDevice should be delayed until system information process is finished
+        if (mOldDeviceInfo != null
+                && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress) {
+            Slog.d(TAG, "Start NewDeviceAction with old deviceInfo:["
+                    + mOldDeviceInfo.toString() + "]");
+        } else {
+            // Add the device ahead with default information to handle <Active Source>
+            // promptly, rather than waiting till the new device action is finished.
+            Slog.d(TAG, "Start NewDeviceAction with default deviceInfo");
+            HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder()
+                    .setLogicalAddress(mDeviceLogicalAddress)
+                    .setPhysicalAddress(mDevicePhysicalAddress)
+                    .setPortId(tv().getPortId(mDevicePhysicalAddress))
+                    .setDeviceType(mDeviceType)
+                    .setVendorId(Constants.VENDOR_ID_UNKNOWN)
+                    .build();
+            // If a deviceInfo with same logical address but different physical address exists
+            // We should remove the old deviceInfo first
+            // This will happen if the interval between unplugging and plugging device is too short
+            // and HotplugDetection Action fails to remove the old deviceInfo, or when the newly
+            // plugged device violates HDMI Spec and uses an occupied logical address
+            if (mOldDeviceInfo != null) {
+                Slog.d(TAG, "Remove device by NewDeviceAction, logical address conflicts: "
+                        + mDevicePhysicalAddress);
+                localDevice().mService.getHdmiCecNetwork().removeCecDevice(
+                        localDevice(), mDeviceLogicalAddress);
+            }
+            localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo);
+        }
         requestOsdName(true);
         return true;
     }
@@ -182,14 +215,30 @@
                 .setVendorId(mVendorId)
                 .setDisplayName(mDisplayName)
                 .build();
-        localDevice().mService.getHdmiCecNetwork().updateCecDevice(deviceInfo);
 
-        // Consume CEC messages we already got for this newly found device.
-        tv().processDelayedMessages(mDeviceLogicalAddress);
+        // Check if oldDevice is same as newDevice
+        // If so, don't add newDevice info, preventing ARC or HDMI source re-connection
+        if (mOldDeviceInfo != null
+                && mOldDeviceInfo.getLogicalAddress() == mDeviceLogicalAddress
+                && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress
+                && mOldDeviceInfo.getDeviceType() == mDeviceType
+                && mOldDeviceInfo.getVendorId() == mVendorId
+                && mOldDeviceInfo.getDisplayName().equals(mDisplayName)) {
+            // Consume CEC messages we already got for this newly found device.
+            tv().processDelayedMessages(mDeviceLogicalAddress);
+            Slog.d(TAG, "Ignore NewDevice, deviceInfo is same as current device");
+            Slog.d(TAG, "Old:[" + mOldDeviceInfo.toString()
+                    + "]; New:[" + deviceInfo.toString() + "]");
+        } else {
+            Slog.d(TAG, "Add NewDevice:[" + deviceInfo.toString() + "]");
+            localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo);
 
-        if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM,
-                mDeviceLogicalAddress)) {
-            tv().onNewAvrAdded(deviceInfo);
+            // Consume CEC messages we already got for this newly found device.
+            tv().processDelayedMessages(mDeviceLogicalAddress);
+            if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM,
+                        mDeviceLogicalAddress)) {
+                tv().onNewAvrAdded(deviceInfo);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 8580b96..6236e2b 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -76,6 +76,8 @@
 import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.XmlUtils;
+import com.android.server.LocalServices;
+import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent;
 import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria;
 import com.android.server.inputmethod.InputMethodManagerInternal;
@@ -197,7 +199,7 @@
         final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice);
         KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId);
         if (config == null) {
-            config = new KeyboardConfiguration();
+            config = new KeyboardConfiguration(deviceId);
             mConfiguredKeyboards.put(deviceId, config);
         }
 
@@ -1093,19 +1095,26 @@
 
     @MainThread
     private void maybeUpdateNotification() {
-        if (mConfiguredKeyboards.size() == 0) {
-            hideKeyboardLayoutNotification();
-            return;
-        }
+        List<KeyboardConfiguration> configurations = new ArrayList<>();
         for (int i = 0; i < mConfiguredKeyboards.size(); i++) {
+            int deviceId = mConfiguredKeyboards.keyAt(i);
+            KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i);
+            if (isVirtualDevice(deviceId)) {
+                continue;
+            }
             // If we have a keyboard with no selected layouts, we should always show missing
             // layout notification even if there are other keyboards that are configured properly.
-            if (!mConfiguredKeyboards.valueAt(i).hasConfiguredLayouts()) {
+            if (!config.hasConfiguredLayouts()) {
                 showMissingKeyboardLayoutNotification();
                 return;
             }
+            configurations.add(config);
         }
-        showConfiguredKeyboardLayoutNotification();
+        if (configurations.size() == 0) {
+            hideKeyboardLayoutNotification();
+            return;
+        }
+        showConfiguredKeyboardLayoutNotification(configurations);
     }
 
     @MainThread
@@ -1185,10 +1194,11 @@
     }
 
     @MainThread
-    private void showConfiguredKeyboardLayoutNotification() {
+    private void showConfiguredKeyboardLayoutNotification(
+            List<KeyboardConfiguration> configurations) {
         final Resources r = mContext.getResources();
 
-        if (mConfiguredKeyboards.size() != 1) {
+        if (configurations.size() != 1) {
             showKeyboardLayoutNotification(
                     r.getString(R.string.keyboard_layout_notification_multiple_selected_title),
                     r.getString(R.string.keyboard_layout_notification_multiple_selected_message),
@@ -1196,8 +1206,8 @@
             return;
         }
 
-        final InputDevice inputDevice = getInputDevice(mConfiguredKeyboards.keyAt(0));
-        final KeyboardConfiguration config = mConfiguredKeyboards.valueAt(0);
+        final KeyboardConfiguration config = configurations.get(0);
+        final InputDevice inputDevice = getInputDevice(config.getDeviceId());
         if (inputDevice == null || !config.hasConfiguredLayouts()) {
             return;
         }
@@ -1356,6 +1366,13 @@
         return false;
     }
 
+    @VisibleForTesting
+    public boolean isVirtualDevice(int deviceId) {
+        VirtualDeviceManagerInternal vdm = LocalServices.getService(
+                VirtualDeviceManagerInternal.class);
+        return vdm == null || vdm.isInputDeviceOwnedByVirtualDevice(deviceId);
+    }
+
     private static int[] getScriptCodes(@Nullable Locale locale) {
         if (locale == null) {
             return new int[0];
@@ -1430,11 +1447,22 @@
     }
 
     private static class KeyboardConfiguration {
+
         // If null or empty, it means no layout is configured for the device. And user needs to
         // manually set up the device.
         @Nullable
         private Set<String> mConfiguredLayouts;
 
+        private final int mDeviceId;
+
+        private KeyboardConfiguration(int deviceId) {
+            mDeviceId = deviceId;
+        }
+
+        private int getDeviceId() {
+            return mDeviceId;
+        }
+
         private boolean hasConfiguredLayouts() {
             return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty();
         }
diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java
index 2934640..21b952b 100644
--- a/services/core/java/com/android/server/inputmethod/ClientController.java
+++ b/services/core/java/com/android/server/inputmethod/ClientController.java
@@ -25,9 +25,13 @@
 import android.view.inputmethod.InputBinding;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.inputmethod.IInputMethodClient;
 import com.android.internal.inputmethod.IRemoteInputConnection;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Store and manage {@link InputMethodManagerService} clients. This class was designed to be a
  * singleton in {@link InputMethodManagerService} since it stores information about all clients,
@@ -37,9 +41,7 @@
  * As part of the re-architecture plan (described in go/imms-rearchitecture-plan), the following
  * fields and methods will be moved out from IMMS and placed here:
  * <ul>
- * <li>mCurClient (ClientState)</li>
  * <li>mClients (ArrayMap of ClientState indexed by IBinder)</li>
- * <li>mLastSwitchUserId</li>
  * </ul>
  * <p>
  * Nested Classes (to move from IMMS):
@@ -54,7 +56,6 @@
  * <li>removeClient</li>
  * <li>verifyClientAndPackageMatch</li>
  * <li>setImeTraceEnabledForAllClients (make it reactive)</li>
- * <li>unbindCurrentClient</li>
  * </ul>
  */
 // TODO(b/314150112): Update the Javadoc above, by removing the re-architecture steps, once this
@@ -65,18 +66,32 @@
     @GuardedBy("ImfLock.class")
     final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
 
+    @GuardedBy("ImfLock.class")
+    private final List<ClientControllerCallback> mCallbacks = new ArrayList<>();
+
     private final PackageManagerInternal mPackageManagerInternal;
 
+    interface ClientControllerCallback {
+
+        void onClientRemoved(ClientState client);
+    }
+
     ClientController(PackageManagerInternal packageManagerInternal) {
         mPackageManagerInternal = packageManagerInternal;
     }
 
     @GuardedBy("ImfLock.class")
-    void addClient(IInputMethodClientInvoker clientInvoker,
-            IRemoteInputConnection inputConnection,
-            int selfReportedDisplayId, IBinder.DeathRecipient deathRecipient, int callerUid,
+    ClientState addClient(IInputMethodClientInvoker clientInvoker,
+            IRemoteInputConnection inputConnection, int selfReportedDisplayId, int callerUid,
             int callerPid) {
-        // TODO: Optimize this linear search.
+        final IBinder.DeathRecipient deathRecipient = () -> {
+            // Exceptionally holding ImfLock here since this is a internal lambda expression.
+            synchronized (ImfLock.class) {
+                removeClientAsBinder(clientInvoker.asBinder());
+            }
+        };
+
+        // TODO(b/319457906): Optimize this linear search.
         final int numClients = mClients.size();
         for (int i = 0; i < numClients; ++i) {
             final ClientState state = mClients.valueAt(i);
@@ -101,14 +116,40 @@
         // have the client crash.  Thus we do not verify the display ID at all here.  Instead we
         // later check the display ID every time the client needs to interact with the specified
         // display.
-        mClients.put(clientInvoker.asBinder(), new ClientState(clientInvoker, inputConnection,
-                callerUid, callerPid, selfReportedDisplayId, deathRecipient));
+        final ClientState cs = new ClientState(clientInvoker, inputConnection,
+                callerUid, callerPid, selfReportedDisplayId, deathRecipient);
+        mClients.put(clientInvoker.asBinder(), cs);
+        return cs;
+    }
+
+    @VisibleForTesting
+    @GuardedBy("ImfLock.class")
+    boolean removeClient(IInputMethodClient client) {
+        return removeClientAsBinder(client.asBinder());
+    }
+
+    @GuardedBy("ImfLock.class")
+    private boolean removeClientAsBinder(IBinder binder) {
+        final ClientState cs = mClients.remove(binder);
+        if (cs == null) {
+            return false;
+        }
+        binder.unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            mCallbacks.get(i).onClientRemoved(cs);
+        }
+        return true;
+    }
+
+    @GuardedBy("ImfLock.class")
+    void addClientControllerCallback(ClientControllerCallback callback) {
+        mCallbacks.add(callback);
     }
 
     @GuardedBy("ImfLock.class")
     boolean verifyClientAndPackageMatch(
             @NonNull IInputMethodClient client, @NonNull String packageName) {
-        ClientState cs = mClients.get(client.asBinder());
+        final ClientState cs = mClients.get(client.asBinder());
         if (cs == null) {
             throw new IllegalArgumentException("unknown client " + client.asBinder());
         }
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index 898d5a5..ad27c52 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -53,8 +53,7 @@
     @GuardedBy("ImfLock.class")
     void reset(@NonNull ArrayMap<String, InputMethodInfo> methodMap) {
         mSubtypeHandles.clear();
-        final InputMethodUtils.InputMethodSettings settings =
-                new InputMethodUtils.InputMethodSettings(methodMap, mUserId);
+        final InputMethodSettings settings = new InputMethodSettings(methodMap, mUserId);
         for (final InputMethodInfo imi : settings.getEnabledInputMethodListLocked()) {
             if (!imi.shouldShowInInputMethodPicker()) {
                 continue;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 25ec683..8448fc2 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -183,7 +183,6 @@
 import com.android.server.input.InputManagerInternal;
 import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener;
 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
-import com.android.server.inputmethod.InputMethodUtils.InputMethodSettings;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.utils.PriorityDump;
@@ -479,7 +478,6 @@
     /**
      * The client that is currently bound to an input method.
      */
-    // TODO(b/314150112): Move this to ClientController.
     @Nullable
     private ClientState mCurClient;
 
@@ -1677,7 +1675,11 @@
 
         mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
         mVisibilityApplier = new DefaultImeVisibilityApplier(this);
+
         mClientController = new ClientController(mPackageManagerInternal);
+        synchronized (ImfLock.class) {
+            mClientController.addClientControllerCallback(c -> onClientRemoved(c));
+        }
 
         mPreventImeStartupUnlessTextEditor = mRes.getBoolean(
                 com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
@@ -2169,47 +2171,41 @@
         // actually running.
         final int callerUid = Binder.getCallingUid();
         final int callerPid = Binder.getCallingPid();
-
-        // TODO(b/314150112): Move the death recipient logic to ClientController when moving
-        //     removeClient method.
-        final IBinder.DeathRecipient deathRecipient = () -> removeClient(client);
         final IInputMethodClientInvoker clientInvoker =
                 IInputMethodClientInvoker.create(client, mHandler);
         synchronized (ImfLock.class) {
             mClientController.addClient(clientInvoker, inputConnection, selfReportedDisplayId,
-                    deathRecipient, callerUid, callerPid);
+                    callerUid, callerPid);
         }
     }
 
-    // TODO(b/314150112): Move this to ClientController.
-    void removeClient(IInputMethodClient client) {
+    // TODO(b/314150112): Move this method to InputMethodBindingController
+    /**
+     * Hide the IME if the removed user is the current user.
+     */
+    private void onClientRemoved(ClientController.ClientState client) {
         synchronized (ImfLock.class) {
-            ClientState cs = mClientController.mClients.remove(client.asBinder());
-            if (cs != null) {
-                client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
-                clearClientSessionLocked(cs);
-                clearClientSessionForAccessibilityLocked(cs);
-
-                if (mCurClient == cs) {
-                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
-                            null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
-                    if (mBoundToMethod) {
-                        mBoundToMethod = false;
-                        IInputMethodInvoker curMethod = getCurMethodLocked();
-                        if (curMethod != null) {
-                            // When we unbind input, we are unbinding the client, so we always
-                            // unbind ime and a11y together.
-                            curMethod.unbindInput();
-                            AccessibilityManagerInternal.get().unbindInput();
-                        }
+            clearClientSessionLocked(client);
+            clearClientSessionForAccessibilityLocked(client);
+            if (mCurClient == client) {
+                hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                        null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+                if (mBoundToMethod) {
+                    mBoundToMethod = false;
+                    IInputMethodInvoker curMethod = getCurMethodLocked();
+                    if (curMethod != null) {
+                        // When we unbind input, we are unbinding the client, so we always
+                        // unbind ime and a11y together.
+                        curMethod.unbindInput();
+                        AccessibilityManagerInternal.get().unbindInput();
                     }
-                    mBoundToAccessibility = false;
-                    mCurClient = null;
                 }
-                if (mCurFocusedWindowClient == cs) {
-                    mCurFocusedWindowClient = null;
-                    mCurFocusedWindowEditorInfo = null;
-                }
+                mBoundToAccessibility = false;
+                mCurClient = null;
+            }
+            if (mCurFocusedWindowClient == client) {
+                mCurFocusedWindowClient = null;
+                mCurFocusedWindowEditorInfo = null;
             }
         }
     }
@@ -2219,8 +2215,7 @@
     void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) {
         if (mCurClient != null) {
             if (DEBUG) {
-                Slog.v(TAG, "unbindCurrentInputLocked: client="
-                        + mCurClient.mClient.asBinder());
+                Slog.v(TAG, "unbindCurrentInputLocked: client=" + mCurClient.mClient.asBinder());
             }
             if (mBoundToMethod) {
                 mBoundToMethod = false;
@@ -2313,7 +2308,8 @@
         final StartInputInfo info = new StartInputInfo(mSettings.getCurrentUserId(),
                 getCurTokenLocked(),
                 mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting,
-                UserHandle.getUserId(mCurClient.mUid), mCurClient.mSelfReportedDisplayId,
+                UserHandle.getUserId(mCurClient.mUid),
+                mCurClient.mSelfReportedDisplayId,
                 mCurFocusedWindow, mCurEditorInfo, mCurFocusedWindowSoftInputMode,
                 getSequenceNumberLocked());
         mImeTargetWindowMap.put(startInputToken, mCurFocusedWindow);
@@ -2324,14 +2320,14 @@
         // same-user scenarios.
         // That said ignoring cross-user scenario will never affect IMEs that do not have
         // INTERACT_ACROSS_USERS(_FULL) permissions, which is actually almost always the case.
-        if (mSettings.getCurrentUserId() == UserHandle.getUserId(mCurClient.mUid)) {
+        if (mSettings.getCurrentUserId() == UserHandle.getUserId(
+                mCurClient.mUid)) {
             mPackageManagerInternal.grantImplicitAccess(mSettings.getCurrentUserId(),
                     null /* intent */, UserHandle.getAppId(getCurMethodUidLocked()),
                     mCurClient.mUid, true /* direct */);
         }
 
-        @InputMethodNavButtonFlags
-        final int navButtonFlags = getInputMethodNavButtonFlagsLocked();
+        @InputMethodNavButtonFlags final int navButtonFlags = getInputMethodNavButtonFlagsLocked();
         final SessionState session = mCurClient.mCurSession;
         setEnabledSessionLocked(session);
         session.mMethod.startInput(startInputToken, mCurInputConnection, mCurEditorInfo, restarting,
@@ -2751,8 +2747,8 @@
                         && curMethod.asBinder() == method.asBinder()) {
                     if (mCurClient != null) {
                         clearClientSessionLocked(mCurClient);
-                        mCurClient.mCurSession = new SessionState(mCurClient,
-                                method, session, channel);
+                        mCurClient.mCurSession = new SessionState(
+                                mCurClient, method, session, channel);
                         InputBindResult res = attachNewInputLocked(
                                 StartInputReason.SESSION_CREATED_BY_IME, true);
                         attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_IME, true);
@@ -4869,8 +4865,8 @@
 
                     final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
                             .getSortedInputMethodAndSubtypeList(
-                                    showAuxSubtypes, isScreenLocked, false, mContext,
-                                    mMethodMap, mSettings.getCurrentUserId());
+                                    showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
+                                    mContext, mMethodMap, mSettings.getCurrentUserId());
                     mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
                             lastInputMethodId, lastInputMethodSubtypeId, imList);
                 }
@@ -5777,8 +5773,10 @@
                 // TODO(b/305829876): Implement user ID verification
                 if (mCurClient != null) {
                     clearClientSessionForAccessibilityLocked(mCurClient, accessibilityConnectionId);
-                    mCurClient.mAccessibilitySessions.put(accessibilityConnectionId,
-                            new AccessibilitySessionState(mCurClient, accessibilityConnectionId,
+                    mCurClient.mAccessibilitySessions.put(
+                            accessibilityConnectionId,
+                            new AccessibilitySessionState(mCurClient,
+                                    accessibilityConnectionId,
                                     session));
 
                     attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_ACCESSIBILITY,
@@ -5812,7 +5810,8 @@
                     }
                     // A11yManagerService unbinds the disabled accessibility service. We don't need
                     // to do it here.
-                    mCurClient.mClient.onUnbindAccessibilityService(getSequenceNumberLocked(),
+                    mCurClient.mClient.onUnbindAccessibilityService(
+                            getSequenceNumberLocked(),
                             accessibilityConnectionId);
                 }
                 // We only have sessions when we bound to an input method. Remove this session
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
new file mode 100644
index 0000000..4c7d755
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
@@ -0,0 +1,686 @@
+/*
+ * 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.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.pm.PackageManagerInternal;
+import android.os.LocaleList;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.IntArray;
+import android.util.Pair;
+import android.util.Printer;
+import android.util.Slog;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * Utility class for putting and getting settings for InputMethod.
+ *
+ * <p>This is used in two ways:</p>
+ * <ul>
+ *     <li>Singleton instance in {@link InputMethodManagerService}, which is updated on
+ *     user-switch to follow the current user.</li>
+ *     <li>On-demand instances when we need settings for non-current users.</li>
+ * </ul>
+ */
+final class InputMethodSettings {
+    public static final boolean DEBUG = false;
+    private static final String TAG = "InputMethodSettings";
+
+    private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
+    private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
+    private static final char INPUT_METHOD_SEPARATOR = InputMethodUtils.INPUT_METHOD_SEPARATOR;
+    private static final char INPUT_METHOD_SUBTYPE_SEPARATOR =
+            InputMethodUtils.INPUT_METHOD_SUBTYPE_SEPARATOR;
+
+    private final ArrayMap<String, InputMethodInfo> mMethodMap;
+    @UserIdInt
+    private final int mCurrentUserId;
+
+    private static void buildEnabledInputMethodsSettingString(
+            StringBuilder builder, Pair<String, ArrayList<String>> ime) {
+        builder.append(ime.first);
+        // Inputmethod and subtypes are saved in the settings as follows:
+        // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
+        for (String subtypeId : ime.second) {
+            builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
+        }
+    }
+
+    InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+        mMethodMap = methodMap;
+        mCurrentUserId = userId;
+        String ime = getSelectedInputMethod();
+        String defaultDeviceIme = getSelectedDefaultDeviceInputMethod();
+        if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) {
+            putSelectedInputMethod(defaultDeviceIme);
+            putSelectedDefaultDeviceInputMethod(null);
+        }
+    }
+
+    private void putString(@NonNull String key, @Nullable String str) {
+        SecureSettingsWrapper.putString(key, str, mCurrentUserId);
+    }
+
+    @Nullable
+    private String getString(@NonNull String key, @Nullable String defaultValue) {
+        return SecureSettingsWrapper.getString(key, defaultValue, mCurrentUserId);
+    }
+
+    private void putInt(String key, int value) {
+        SecureSettingsWrapper.putInt(key, value, mCurrentUserId);
+    }
+
+    private int getInt(String key, int defaultValue) {
+        return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId);
+    }
+
+    ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
+        return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */);
+    }
+
+    @NonNull
+    ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked(
+            @Nullable Predicate<InputMethodInfo> matchingCondition) {
+        return createEnabledInputMethodListLocked(
+                getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition);
+    }
+
+    List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
+            InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) {
+        List<InputMethodSubtype> enabledSubtypes =
+                getEnabledInputMethodSubtypeListLocked(imi);
+        if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) {
+            enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+                    SystemLocaleWrapper.get(mCurrentUserId), imi);
+        }
+        return InputMethodSubtype.sort(imi, enabledSubtypes);
+    }
+
+    List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) {
+        List<Pair<String, ArrayList<String>>> imsList =
+                getEnabledInputMethodsAndSubtypeListLocked();
+        ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
+        if (imi != null) {
+            for (Pair<String, ArrayList<String>> imsPair : imsList) {
+                InputMethodInfo info = mMethodMap.get(imsPair.first);
+                if (info != null && info.getId().equals(imi.getId())) {
+                    final int subtypeCount = info.getSubtypeCount();
+                    for (int i = 0; i < subtypeCount; ++i) {
+                        InputMethodSubtype ims = info.getSubtypeAt(i);
+                        for (String s : imsPair.second) {
+                            if (String.valueOf(ims.hashCode()).equals(s)) {
+                                enabledSubtypes.add(ims);
+                            }
+                        }
+                    }
+                    break;
+                }
+            }
+        }
+        return enabledSubtypes;
+    }
+
+    List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
+        final String enabledInputMethodsStr = getEnabledInputMethodsStr();
+        final TextUtils.SimpleStringSplitter inputMethodSplitter =
+                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
+        final TextUtils.SimpleStringSplitter subtypeSplitter =
+                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
+        final ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
+        if (TextUtils.isEmpty(enabledInputMethodsStr)) {
+            return imsList;
+        }
+        inputMethodSplitter.setString(enabledInputMethodsStr);
+        while (inputMethodSplitter.hasNext()) {
+            String nextImsStr = inputMethodSplitter.next();
+            subtypeSplitter.setString(nextImsStr);
+            if (subtypeSplitter.hasNext()) {
+                ArrayList<String> subtypeHashes = new ArrayList<>();
+                // The first element is ime id.
+                String imeId = subtypeSplitter.next();
+                while (subtypeSplitter.hasNext()) {
+                    subtypeHashes.add(subtypeSplitter.next());
+                }
+                imsList.add(new Pair<>(imeId, subtypeHashes));
+            }
+        }
+        return imsList;
+    }
+
+    /**
+     * Build and put a string of EnabledInputMethods with removing specified Id.
+     *
+     * @return the specified id was removed or not.
+     */
+    boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+            StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
+        boolean isRemoved = false;
+        boolean needsAppendSeparator = false;
+        for (Pair<String, ArrayList<String>> ims : imsList) {
+            String curId = ims.first;
+            if (curId.equals(id)) {
+                // We are disabling this input method, and it is
+                // currently enabled.  Skip it to remove from the
+                // new list.
+                isRemoved = true;
+            } else {
+                if (needsAppendSeparator) {
+                    builder.append(INPUT_METHOD_SEPARATOR);
+                } else {
+                    needsAppendSeparator = true;
+                }
+                buildEnabledInputMethodsSettingString(builder, ims);
+            }
+        }
+        if (isRemoved) {
+            // Update the setting with the new list of input methods.
+            putEnabledInputMethodsStr(builder.toString());
+        }
+        return isRemoved;
+    }
+
+    private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
+            List<Pair<String, ArrayList<String>>> imsList,
+            Predicate<InputMethodInfo> matchingCondition) {
+        final ArrayList<InputMethodInfo> res = new ArrayList<>();
+        for (Pair<String, ArrayList<String>> ims : imsList) {
+            InputMethodInfo info = mMethodMap.get(ims.first);
+            if (info != null && !info.isVrOnly()
+                    && (matchingCondition == null || matchingCondition.test(info))) {
+                res.add(info);
+            }
+        }
+        return res;
+    }
+
+    void putEnabledInputMethodsStr(@Nullable String str) {
+        if (DEBUG) {
+            Slog.d(TAG, "putEnabledInputMethodStr: " + str);
+        }
+        if (TextUtils.isEmpty(str)) {
+            // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
+            // empty data scenario.
+            putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
+        } else {
+            putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
+        }
+    }
+
+    @NonNull
+    String getEnabledInputMethodsStr() {
+        return getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
+    }
+
+    private void saveSubtypeHistory(
+            List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
+        StringBuilder builder = new StringBuilder();
+        boolean isImeAdded = false;
+        if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
+            builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
+                    newSubtypeId);
+            isImeAdded = true;
+        }
+        for (Pair<String, String> ime : savedImes) {
+            String imeId = ime.first;
+            String subtypeId = ime.second;
+            if (TextUtils.isEmpty(subtypeId)) {
+                subtypeId = NOT_A_SUBTYPE_ID_STR;
+            }
+            if (isImeAdded) {
+                builder.append(INPUT_METHOD_SEPARATOR);
+            } else {
+                isImeAdded = true;
+            }
+            builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
+                    subtypeId);
+        }
+        // Remove the last INPUT_METHOD_SEPARATOR
+        putSubtypeHistoryStr(builder.toString());
+    }
+
+    private void addSubtypeToHistory(String imeId, String subtypeId) {
+        List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
+        for (Pair<String, String> ime : subtypeHistory) {
+            if (ime.first.equals(imeId)) {
+                if (DEBUG) {
+                    Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
+                            + ime.second);
+                }
+                // We should break here
+                subtypeHistory.remove(ime);
+                break;
+            }
+        }
+        if (DEBUG) {
+            Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
+        }
+        saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
+    }
+
+    private void putSubtypeHistoryStr(@NonNull String str) {
+        if (DEBUG) {
+            Slog.d(TAG, "putSubtypeHistoryStr: " + str);
+        }
+        if (TextUtils.isEmpty(str)) {
+            // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
+            // data scenario.
+            putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
+        } else {
+            putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
+        }
+    }
+
+    Pair<String, String> getLastInputMethodAndSubtypeLocked() {
+        // Gets the first one from the history
+        return getLastSubtypeForInputMethodLockedInternal(null);
+    }
+
+    @Nullable
+    InputMethodSubtype getLastInputMethodSubtypeLocked() {
+        final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked();
+        // TODO: Handle the case of the last IME with no subtypes
+        if (lastIme == null || TextUtils.isEmpty(lastIme.first)
+                || TextUtils.isEmpty(lastIme.second)) {
+            return null;
+        }
+        final InputMethodInfo lastImi = mMethodMap.get(lastIme.first);
+        if (lastImi == null) return null;
+        try {
+            final int lastSubtypeHash = Integer.parseInt(lastIme.second);
+            final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi,
+                    lastSubtypeHash);
+            if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) {
+                return null;
+            }
+            return lastImi.getSubtypeAt(lastSubtypeId);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    String getLastSubtypeForInputMethodLocked(String imeId) {
+        Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
+        if (ime != null) {
+            return ime.second;
+        } else {
+            return null;
+        }
+    }
+
+    private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
+        List<Pair<String, ArrayList<String>>> enabledImes =
+                getEnabledInputMethodsAndSubtypeListLocked();
+        List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
+        for (Pair<String, String> imeAndSubtype : subtypeHistory) {
+            final String imeInTheHistory = imeAndSubtype.first;
+            // If imeId is empty, returns the first IME and subtype in the history
+            if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
+                final String subtypeInTheHistory = imeAndSubtype.second;
+                final String subtypeHashCode =
+                        getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
+                                enabledImes, imeInTheHistory, subtypeInTheHistory);
+                if (!TextUtils.isEmpty(subtypeHashCode)) {
+                    if (DEBUG) {
+                        Slog.d(TAG,
+                                "Enabled subtype found in the history: " + subtypeHashCode);
+                    }
+                    return new Pair<>(imeInTheHistory, subtypeHashCode);
+                }
+            }
+        }
+        if (DEBUG) {
+            Slog.d(TAG, "No enabled IME found in the history");
+        }
+        return null;
+    }
+
+    private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
+            ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
+        final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId);
+        for (Pair<String, ArrayList<String>> enabledIme : enabledImes) {
+            if (enabledIme.first.equals(imeId)) {
+                final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
+                final InputMethodInfo imi = mMethodMap.get(imeId);
+                if (explicitlyEnabledSubtypes.size() == 0) {
+                    // If there are no explicitly enabled subtypes, applicable subtypes are
+                    // enabled implicitly.
+                    // If IME is enabled and no subtypes are enabled, applicable subtypes
+                    // are enabled implicitly, so needs to treat them to be enabled.
+                    if (imi != null && imi.getSubtypeCount() > 0) {
+                        List<InputMethodSubtype> implicitlyEnabledSubtypes =
+                                SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList,
+                                        imi);
+                        final int numSubtypes = implicitlyEnabledSubtypes.size();
+                        for (int i = 0; i < numSubtypes; ++i) {
+                            final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i);
+                            if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
+                                return subtypeHashCode;
+                            }
+                        }
+                    }
+                } else {
+                    for (String s : explicitlyEnabledSubtypes) {
+                        if (s.equals(subtypeHashCode)) {
+                            // If both imeId and subtypeId are enabled, return subtypeId.
+                            try {
+                                final int hashCode = Integer.parseInt(subtypeHashCode);
+                                // Check whether the subtype id is valid or not
+                                if (SubtypeUtils.isValidSubtypeId(imi, hashCode)) {
+                                    return s;
+                                } else {
+                                    return NOT_A_SUBTYPE_ID_STR;
+                                }
+                            } catch (NumberFormatException e) {
+                                return NOT_A_SUBTYPE_ID_STR;
+                            }
+                        }
+                    }
+                }
+                // If imeId was enabled but subtypeId was disabled.
+                return NOT_A_SUBTYPE_ID_STR;
+            }
+        }
+        // If both imeId and subtypeId are disabled, return null
+        return null;
+    }
+
+    private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
+        ArrayList<Pair<String, String>> imsList = new ArrayList<>();
+        final String subtypeHistoryStr = getSubtypeHistoryStr();
+        if (TextUtils.isEmpty(subtypeHistoryStr)) {
+            return imsList;
+        }
+        final TextUtils.SimpleStringSplitter inputMethodSplitter =
+                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
+        final TextUtils.SimpleStringSplitter subtypeSplitter =
+                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
+        inputMethodSplitter.setString(subtypeHistoryStr);
+        while (inputMethodSplitter.hasNext()) {
+            String nextImsStr = inputMethodSplitter.next();
+            subtypeSplitter.setString(nextImsStr);
+            if (subtypeSplitter.hasNext()) {
+                String subtypeId = NOT_A_SUBTYPE_ID_STR;
+                // The first element is ime id.
+                String imeId = subtypeSplitter.next();
+                while (subtypeSplitter.hasNext()) {
+                    subtypeId = subtypeSplitter.next();
+                    break;
+                }
+                imsList.add(new Pair<>(imeId, subtypeId));
+            }
+        }
+        return imsList;
+    }
+
+    @NonNull
+    private String getSubtypeHistoryStr() {
+        final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
+        if (DEBUG) {
+            Slog.d(TAG, "getSubtypeHistoryStr: " + history);
+        }
+        return history;
+    }
+
+    void putSelectedInputMethod(String imeId) {
+        if (DEBUG) {
+            Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
+                    + mCurrentUserId);
+        }
+        putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
+    }
+
+    void putSelectedSubtype(int subtypeId) {
+        if (DEBUG) {
+            Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
+                    + mCurrentUserId);
+        }
+        putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
+    }
+
+    @Nullable
+    String getSelectedInputMethod() {
+        final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
+        if (DEBUG) {
+            Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
+        }
+        return imi;
+    }
+
+    @Nullable
+    String getSelectedDefaultDeviceInputMethod() {
+        final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null);
+        if (DEBUG) {
+            Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", "
+                    + mCurrentUserId);
+        }
+        return imi;
+    }
+
+    void putSelectedDefaultDeviceInputMethod(String imeId) {
+        if (DEBUG) {
+            Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", "
+                    + mCurrentUserId);
+        }
+        putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId);
+    }
+
+    void putDefaultVoiceInputMethod(String imeId) {
+        if (DEBUG) {
+            Slog.d(TAG,
+                    "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId);
+        }
+        putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId);
+    }
+
+    @Nullable
+    String getDefaultVoiceInputMethod() {
+        final String imi = getString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, null);
+        if (DEBUG) {
+            Slog.d(TAG, "getDefaultVoiceInputMethodStr: " + imi);
+        }
+        return imi;
+    }
+
+    boolean isSubtypeSelected() {
+        return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
+    }
+
+    private int getSelectedInputMethodSubtypeHashCode() {
+        return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
+                NOT_A_SUBTYPE_ID);
+    }
+
+    @UserIdInt
+    public int getCurrentUserId() {
+        return mCurrentUserId;
+    }
+
+    int getSelectedInputMethodSubtypeId(String selectedImiId) {
+        final InputMethodInfo imi = mMethodMap.get(selectedImiId);
+        if (imi == null) {
+            return NOT_A_SUBTYPE_ID;
+        }
+        final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
+        return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode);
+    }
+
+    void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId,
+            InputMethodSubtype currentSubtype) {
+        String subtypeId = NOT_A_SUBTYPE_ID_STR;
+        if (currentSubtype != null) {
+            subtypeId = String.valueOf(currentSubtype.hashCode());
+        }
+        if (InputMethodUtils.canAddToLastInputMethod(currentSubtype)) {
+            addSubtypeToHistory(curMethodId, subtypeId);
+        }
+    }
+
+    /**
+     * A variant of {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()} for
+     * non-current users.
+     *
+     * <p>TODO: Address code duplication between this and
+     * {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()}.</p>
+     *
+     * @return {@link InputMethodSubtype} if exists. {@code null} otherwise.
+     */
+    @Nullable
+    InputMethodSubtype getCurrentInputMethodSubtypeForNonCurrentUsers() {
+        final String selectedMethodId = getSelectedInputMethod();
+        if (selectedMethodId == null) {
+            return null;
+        }
+        final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
+        if (imi == null || imi.getSubtypeCount() == 0) {
+            return null;
+        }
+
+        final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
+        if (subtypeHashCode != NOT_A_SUBTYPE_ID) {
+            final int subtypeIndex = SubtypeUtils.getSubtypeIdFromHashCode(imi,
+                    subtypeHashCode);
+            if (subtypeIndex >= 0) {
+                return imi.getSubtypeAt(subtypeIndex);
+            }
+        }
+
+        // If there are no selected subtypes, the framework will try to find the most applicable
+        // subtype from explicitly or implicitly enabled subtypes.
+        final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
+                getEnabledInputMethodSubtypeListLocked(imi, true);
+        // If there is only one explicitly or implicitly enabled subtype, just returns it.
+        if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) {
+            return null;
+        }
+        if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
+            return explicitlyOrImplicitlyEnabledSubtypes.get(0);
+        }
+        final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString();
+        final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+                explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD,
+                locale, true);
+        if (subtype != null) {
+            return subtype;
+        }
+        return SubtypeUtils.findLastResortApplicableSubtypeLocked(
+                explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
+    }
+
+    boolean setAdditionalInputMethodSubtypes(@NonNull String imeId,
+            @NonNull ArrayList<InputMethodSubtype> subtypes,
+            @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
+            @NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
+        final InputMethodInfo imi = mMethodMap.get(imeId);
+        if (imi == null) {
+            return false;
+        }
+        if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
+                imi.getPackageName())) {
+            return false;
+        }
+
+        if (subtypes.isEmpty()) {
+            additionalSubtypeMap.remove(imi.getId());
+        } else {
+            additionalSubtypeMap.put(imi.getId(), subtypes);
+        }
+        AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId());
+        return true;
+    }
+
+    boolean setEnabledInputMethodSubtypes(@NonNull String imeId,
+            @NonNull int[] subtypeHashCodes) {
+        final InputMethodInfo imi = mMethodMap.get(imeId);
+        if (imi == null) {
+            return false;
+        }
+
+        final IntArray validSubtypeHashCodes = new IntArray(subtypeHashCodes.length);
+        for (int subtypeHashCode : subtypeHashCodes) {
+            if (subtypeHashCode == NOT_A_SUBTYPE_ID) {
+                continue;  // NOT_A_SUBTYPE_ID must not be saved
+            }
+            if (!SubtypeUtils.isValidSubtypeId(imi, subtypeHashCode)) {
+                continue;  // this subtype does not exist in InputMethodInfo.
+            }
+            if (validSubtypeHashCodes.indexOf(subtypeHashCode) >= 0) {
+                continue;  // The entry is already added.  No need to add anymore.
+            }
+            validSubtypeHashCodes.add(subtypeHashCode);
+        }
+
+        final String originalEnabledImesString = getEnabledInputMethodsStr();
+        final String updatedEnabledImesString = updateEnabledImeString(
+                originalEnabledImesString, imi.getId(), validSubtypeHashCodes);
+        if (TextUtils.equals(originalEnabledImesString, updatedEnabledImesString)) {
+            return false;
+        }
+
+        putEnabledInputMethodsStr(updatedEnabledImesString);
+        return true;
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static String updateEnabledImeString(@NonNull String enabledImesString,
+            @NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes) {
+        final TextUtils.SimpleStringSplitter imeSplitter =
+                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
+        final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
+                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
+
+        final StringBuilder sb = new StringBuilder();
+
+        imeSplitter.setString(enabledImesString);
+        boolean needsImeSeparator = false;
+        while (imeSplitter.hasNext()) {
+            final String nextImsStr = imeSplitter.next();
+            imeSubtypeSplitter.setString(nextImsStr);
+            if (imeSubtypeSplitter.hasNext()) {
+                if (needsImeSeparator) {
+                    sb.append(INPUT_METHOD_SEPARATOR);
+                }
+                if (TextUtils.equals(imeId, imeSubtypeSplitter.next())) {
+                    sb.append(imeId);
+                    for (int i = 0; i < enabledSubtypeHashCodes.size(); ++i) {
+                        sb.append(INPUT_METHOD_SUBTYPE_SEPARATOR);
+                        sb.append(enabledSubtypeHashCodes.get(i));
+                    }
+                } else {
+                    sb.append(nextImsStr);
+                }
+                needsImeSeparator = true;
+            }
+        }
+        return sb.toString();
+    }
+
+    void dumpLocked(final Printer pw, final String prefix) {
+        pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index 4439b06..4305808 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -31,7 +31,6 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.inputmethod.InputMethodUtils.InputMethodSettings;
 
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index fb57c09..361cdbb 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -28,15 +28,10 @@
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.os.Build;
-import android.os.LocaleList;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
-import android.util.ArrayMap;
 import android.util.ArraySet;
-import android.util.IntArray;
-import android.util.Pair;
-import android.util.Printer;
 import android.util.Slog;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
@@ -51,10 +46,8 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import java.util.StringJoiner;
 import java.util.function.Consumer;
-import java.util.function.Predicate;
 
 /**
  * This class provides random static utility methods for {@link InputMethodManagerService} and its
@@ -68,12 +61,11 @@
     public static final boolean DEBUG = false;
     static final int NOT_A_SUBTYPE_ID = -1;
     private static final String TAG = "InputMethodUtils";
-    private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
 
     // The string for enabled input method is saved as follows:
     // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
-    private static final char INPUT_METHOD_SEPARATOR = ':';
-    private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
+    static final char INPUT_METHOD_SEPARATOR = ':';
+    static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
 
     private InputMethodUtils() {
         // This utility class is not publicly instantiable.
@@ -200,648 +192,6 @@
             UserHandle.getUserId(uid));
     }
 
-    /**
-     * Utility class for putting and getting settings for InputMethod.
-     *
-     * This is used in two ways:
-     * - Singleton instance in {@link InputMethodManagerService}, which is updated on user-switch to
-     * follow the current user.
-     * - On-demand instances when we need settings for non-current users.
-     *
-     * TODO: Move all putters and getters of settings to this class.
-     */
-    @UserHandleAware
-    public static class InputMethodSettings {
-        private final ArrayMap<String, InputMethodInfo> mMethodMap;
-
-        @UserIdInt
-        private final int mCurrentUserId;
-
-        private static void buildEnabledInputMethodsSettingString(
-                StringBuilder builder, Pair<String, ArrayList<String>> ime) {
-            builder.append(ime.first);
-            // Inputmethod and subtypes are saved in the settings as follows:
-            // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
-            for (String subtypeId: ime.second) {
-                builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
-            }
-        }
-
-        InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
-            mMethodMap = methodMap;
-            mCurrentUserId = userId;
-            String ime = getSelectedInputMethod();
-            String defaultDeviceIme = getSelectedDefaultDeviceInputMethod();
-            if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) {
-                putSelectedInputMethod(defaultDeviceIme);
-                putSelectedDefaultDeviceInputMethod(null);
-            }
-        }
-
-        private void putString(@NonNull String key, @Nullable String str) {
-            SecureSettingsWrapper.putString(key, str, mCurrentUserId);
-        }
-
-        @Nullable
-        private String getString(@NonNull String key, @Nullable String defaultValue) {
-            return SecureSettingsWrapper.getString(key, defaultValue, mCurrentUserId);
-        }
-
-        private void putInt(String key, int value) {
-            SecureSettingsWrapper.putInt(key, value, mCurrentUserId);
-        }
-
-        private int getInt(String key, int defaultValue) {
-            return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId);
-        }
-
-        private void putBoolean(String key, boolean value) {
-            SecureSettingsWrapper.putBoolean(key, value, mCurrentUserId);
-        }
-
-        private boolean getBoolean(String key, boolean defaultValue) {
-            return SecureSettingsWrapper.getBoolean(key, defaultValue, mCurrentUserId);
-        }
-
-        ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
-            return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */);
-        }
-
-        @NonNull
-        ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked(
-                @Nullable Predicate<InputMethodInfo> matchingCondition) {
-            return createEnabledInputMethodListLocked(
-                    getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition);
-        }
-
-        List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
-                InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) {
-            List<InputMethodSubtype> enabledSubtypes =
-                    getEnabledInputMethodSubtypeListLocked(imi);
-            if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) {
-                enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
-                        SystemLocaleWrapper.get(mCurrentUserId), imi);
-            }
-            return InputMethodSubtype.sort(imi, enabledSubtypes);
-        }
-
-        List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) {
-            List<Pair<String, ArrayList<String>>> imsList =
-                    getEnabledInputMethodsAndSubtypeListLocked();
-            ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
-            if (imi != null) {
-                for (Pair<String, ArrayList<String>> imsPair : imsList) {
-                    InputMethodInfo info = mMethodMap.get(imsPair.first);
-                    if (info != null && info.getId().equals(imi.getId())) {
-                        final int subtypeCount = info.getSubtypeCount();
-                        for (int i = 0; i < subtypeCount; ++i) {
-                            InputMethodSubtype ims = info.getSubtypeAt(i);
-                            for (String s: imsPair.second) {
-                                if (String.valueOf(ims.hashCode()).equals(s)) {
-                                    enabledSubtypes.add(ims);
-                                }
-                            }
-                        }
-                        break;
-                    }
-                }
-            }
-            return enabledSubtypes;
-        }
-
-        List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
-            final String enabledInputMethodsStr = getEnabledInputMethodsStr();
-            final TextUtils.SimpleStringSplitter inputMethodSplitter =
-                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
-            final TextUtils.SimpleStringSplitter subtypeSplitter =
-                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
-            final ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
-            if (TextUtils.isEmpty(enabledInputMethodsStr)) {
-                return imsList;
-            }
-            inputMethodSplitter.setString(enabledInputMethodsStr);
-            while (inputMethodSplitter.hasNext()) {
-                String nextImsStr = inputMethodSplitter.next();
-                subtypeSplitter.setString(nextImsStr);
-                if (subtypeSplitter.hasNext()) {
-                    ArrayList<String> subtypeHashes = new ArrayList<>();
-                    // The first element is ime id.
-                    String imeId = subtypeSplitter.next();
-                    while (subtypeSplitter.hasNext()) {
-                        subtypeHashes.add(subtypeSplitter.next());
-                    }
-                    imsList.add(new Pair<>(imeId, subtypeHashes));
-                }
-            }
-            return imsList;
-        }
-
-        /**
-         * Build and put a string of EnabledInputMethods with removing specified Id.
-         * @return the specified id was removed or not.
-         */
-        boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
-                StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
-            boolean isRemoved = false;
-            boolean needsAppendSeparator = false;
-            for (Pair<String, ArrayList<String>> ims: imsList) {
-                String curId = ims.first;
-                if (curId.equals(id)) {
-                    // We are disabling this input method, and it is
-                    // currently enabled.  Skip it to remove from the
-                    // new list.
-                    isRemoved = true;
-                } else {
-                    if (needsAppendSeparator) {
-                        builder.append(INPUT_METHOD_SEPARATOR);
-                    } else {
-                        needsAppendSeparator = true;
-                    }
-                    buildEnabledInputMethodsSettingString(builder, ims);
-                }
-            }
-            if (isRemoved) {
-                // Update the setting with the new list of input methods.
-                putEnabledInputMethodsStr(builder.toString());
-            }
-            return isRemoved;
-        }
-
-        private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
-                List<Pair<String, ArrayList<String>>> imsList,
-                Predicate<InputMethodInfo> matchingCondition) {
-            final ArrayList<InputMethodInfo> res = new ArrayList<>();
-            for (Pair<String, ArrayList<String>> ims: imsList) {
-                InputMethodInfo info = mMethodMap.get(ims.first);
-                if (info != null && !info.isVrOnly()
-                        && (matchingCondition == null || matchingCondition.test(info))) {
-                    res.add(info);
-                }
-            }
-            return res;
-        }
-
-        void putEnabledInputMethodsStr(@Nullable String str) {
-            if (DEBUG) {
-                Slog.d(TAG, "putEnabledInputMethodStr: " + str);
-            }
-            if (TextUtils.isEmpty(str)) {
-                // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
-                // empty data scenario.
-                putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
-            } else {
-                putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
-            }
-        }
-
-        @NonNull
-        String getEnabledInputMethodsStr() {
-            return getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
-        }
-
-        private void saveSubtypeHistory(
-                List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
-            StringBuilder builder = new StringBuilder();
-            boolean isImeAdded = false;
-            if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
-                builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
-                        newSubtypeId);
-                isImeAdded = true;
-            }
-            for (Pair<String, String> ime: savedImes) {
-                String imeId = ime.first;
-                String subtypeId = ime.second;
-                if (TextUtils.isEmpty(subtypeId)) {
-                    subtypeId = NOT_A_SUBTYPE_ID_STR;
-                }
-                if (isImeAdded) {
-                    builder.append(INPUT_METHOD_SEPARATOR);
-                } else {
-                    isImeAdded = true;
-                }
-                builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
-                        subtypeId);
-            }
-            // Remove the last INPUT_METHOD_SEPARATOR
-            putSubtypeHistoryStr(builder.toString());
-        }
-
-        private void addSubtypeToHistory(String imeId, String subtypeId) {
-            List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
-            for (Pair<String, String> ime: subtypeHistory) {
-                if (ime.first.equals(imeId)) {
-                    if (DEBUG) {
-                        Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
-                                + ime.second);
-                    }
-                    // We should break here
-                    subtypeHistory.remove(ime);
-                    break;
-                }
-            }
-            if (DEBUG) {
-                Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
-            }
-            saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
-        }
-
-        private void putSubtypeHistoryStr(@NonNull String str) {
-            if (DEBUG) {
-                Slog.d(TAG, "putSubtypeHistoryStr: " + str);
-            }
-            if (TextUtils.isEmpty(str)) {
-                // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
-                // data scenario.
-                putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
-            } else {
-                putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
-            }
-        }
-
-        Pair<String, String> getLastInputMethodAndSubtypeLocked() {
-            // Gets the first one from the history
-            return getLastSubtypeForInputMethodLockedInternal(null);
-        }
-
-        @Nullable
-        InputMethodSubtype getLastInputMethodSubtypeLocked() {
-            final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked();
-            // TODO: Handle the case of the last IME with no subtypes
-            if (lastIme == null || TextUtils.isEmpty(lastIme.first)
-                    || TextUtils.isEmpty(lastIme.second)) return null;
-            final InputMethodInfo lastImi = mMethodMap.get(lastIme.first);
-            if (lastImi == null) return null;
-            try {
-                final int lastSubtypeHash = Integer.parseInt(lastIme.second);
-                final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi,
-                        lastSubtypeHash);
-                if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) {
-                    return null;
-                }
-                return lastImi.getSubtypeAt(lastSubtypeId);
-            } catch (NumberFormatException e) {
-                return null;
-            }
-        }
-
-        String getLastSubtypeForInputMethodLocked(String imeId) {
-            Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
-            if (ime != null) {
-                return ime.second;
-            } else {
-                return null;
-            }
-        }
-
-        private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
-            List<Pair<String, ArrayList<String>>> enabledImes =
-                    getEnabledInputMethodsAndSubtypeListLocked();
-            List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
-            for (Pair<String, String> imeAndSubtype : subtypeHistory) {
-                final String imeInTheHistory = imeAndSubtype.first;
-                // If imeId is empty, returns the first IME and subtype in the history
-                if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
-                    final String subtypeInTheHistory = imeAndSubtype.second;
-                    final String subtypeHashCode =
-                            getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
-                                    enabledImes, imeInTheHistory, subtypeInTheHistory);
-                    if (!TextUtils.isEmpty(subtypeHashCode)) {
-                        if (DEBUG) {
-                            Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
-                        }
-                        return new Pair<>(imeInTheHistory, subtypeHashCode);
-                    }
-                }
-            }
-            if (DEBUG) {
-                Slog.d(TAG, "No enabled IME found in the history");
-            }
-            return null;
-        }
-
-        private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
-                ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
-            final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId);
-            for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
-                if (enabledIme.first.equals(imeId)) {
-                    final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
-                    final InputMethodInfo imi = mMethodMap.get(imeId);
-                    if (explicitlyEnabledSubtypes.size() == 0) {
-                        // If there are no explicitly enabled subtypes, applicable subtypes are
-                        // enabled implicitly.
-                        // If IME is enabled and no subtypes are enabled, applicable subtypes
-                        // are enabled implicitly, so needs to treat them to be enabled.
-                        if (imi != null && imi.getSubtypeCount() > 0) {
-                            List<InputMethodSubtype> implicitlyEnabledSubtypes =
-                                    SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList,
-                                            imi);
-                            final int numSubtypes = implicitlyEnabledSubtypes.size();
-                            for (int i = 0; i < numSubtypes; ++i) {
-                                final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i);
-                                if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
-                                    return subtypeHashCode;
-                                }
-                            }
-                        }
-                    } else {
-                        for (String s: explicitlyEnabledSubtypes) {
-                            if (s.equals(subtypeHashCode)) {
-                                // If both imeId and subtypeId are enabled, return subtypeId.
-                                try {
-                                    final int hashCode = Integer.parseInt(subtypeHashCode);
-                                    // Check whether the subtype id is valid or not
-                                    if (SubtypeUtils.isValidSubtypeId(imi, hashCode)) {
-                                        return s;
-                                    } else {
-                                        return NOT_A_SUBTYPE_ID_STR;
-                                    }
-                                } catch (NumberFormatException e) {
-                                    return NOT_A_SUBTYPE_ID_STR;
-                                }
-                            }
-                        }
-                    }
-                    // If imeId was enabled but subtypeId was disabled.
-                    return NOT_A_SUBTYPE_ID_STR;
-                }
-            }
-            // If both imeId and subtypeId are disabled, return null
-            return null;
-        }
-
-        private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
-            ArrayList<Pair<String, String>> imsList = new ArrayList<>();
-            final String subtypeHistoryStr = getSubtypeHistoryStr();
-            if (TextUtils.isEmpty(subtypeHistoryStr)) {
-                return imsList;
-            }
-            final TextUtils.SimpleStringSplitter inputMethodSplitter =
-                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
-            final TextUtils.SimpleStringSplitter subtypeSplitter =
-                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
-            inputMethodSplitter.setString(subtypeHistoryStr);
-            while (inputMethodSplitter.hasNext()) {
-                String nextImsStr = inputMethodSplitter.next();
-                subtypeSplitter.setString(nextImsStr);
-                if (subtypeSplitter.hasNext()) {
-                    String subtypeId = NOT_A_SUBTYPE_ID_STR;
-                    // The first element is ime id.
-                    String imeId = subtypeSplitter.next();
-                    while (subtypeSplitter.hasNext()) {
-                        subtypeId = subtypeSplitter.next();
-                        break;
-                    }
-                    imsList.add(new Pair<>(imeId, subtypeId));
-                }
-            }
-            return imsList;
-        }
-
-        @NonNull
-        private String getSubtypeHistoryStr() {
-            final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
-            if (DEBUG) {
-                Slog.d(TAG, "getSubtypeHistoryStr: " + history);
-            }
-            return history;
-        }
-
-        void putSelectedInputMethod(String imeId) {
-            if (DEBUG) {
-                Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
-                        + mCurrentUserId);
-            }
-            putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
-        }
-
-        void putSelectedSubtype(int subtypeId) {
-            if (DEBUG) {
-                Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
-                        + mCurrentUserId);
-            }
-            putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
-        }
-
-        @Nullable
-        String getSelectedInputMethod() {
-            final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
-            if (DEBUG) {
-                Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
-            }
-            return imi;
-        }
-
-        @Nullable
-        String getSelectedDefaultDeviceInputMethod() {
-            final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null);
-            if (DEBUG) {
-                Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", "
-                        + mCurrentUserId);
-            }
-            return imi;
-        }
-
-        void putSelectedDefaultDeviceInputMethod(String imeId) {
-            if (DEBUG) {
-                Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", "
-                        + mCurrentUserId);
-            }
-            putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId);
-        }
-
-        void putDefaultVoiceInputMethod(String imeId) {
-            if (DEBUG) {
-                Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId);
-            }
-            putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId);
-        }
-
-        @Nullable
-        String getDefaultVoiceInputMethod() {
-            final String imi = getString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, null);
-            if (DEBUG) {
-                Slog.d(TAG, "getDefaultVoiceInputMethodStr: " + imi);
-            }
-            return imi;
-        }
-
-        boolean isSubtypeSelected() {
-            return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
-        }
-
-        private int getSelectedInputMethodSubtypeHashCode() {
-            return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
-        }
-
-        @UserIdInt
-        public int getCurrentUserId() {
-            return mCurrentUserId;
-        }
-
-        int getSelectedInputMethodSubtypeId(String selectedImiId) {
-            final InputMethodInfo imi = mMethodMap.get(selectedImiId);
-            if (imi == null) {
-                return NOT_A_SUBTYPE_ID;
-            }
-            final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
-            return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode);
-        }
-
-        void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId,
-                InputMethodSubtype currentSubtype) {
-            String subtypeId = NOT_A_SUBTYPE_ID_STR;
-            if (currentSubtype != null) {
-                subtypeId = String.valueOf(currentSubtype.hashCode());
-            }
-            if (canAddToLastInputMethod(currentSubtype)) {
-                addSubtypeToHistory(curMethodId, subtypeId);
-            }
-        }
-
-        /**
-         * A variant of {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()} for
-         * non-current users.
-         *
-         * <p>TODO: Address code duplication between this and
-         * {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()}.</p>
-         *
-         * @return {@link InputMethodSubtype} if exists. {@code null} otherwise.
-         */
-        @Nullable
-        InputMethodSubtype getCurrentInputMethodSubtypeForNonCurrentUsers() {
-            final String selectedMethodId = getSelectedInputMethod();
-            if (selectedMethodId == null) {
-                return null;
-            }
-            final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
-            if (imi == null || imi.getSubtypeCount() == 0) {
-                return null;
-            }
-
-            final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
-            if (subtypeHashCode != InputMethodUtils.NOT_A_SUBTYPE_ID) {
-                final int subtypeIndex = SubtypeUtils.getSubtypeIdFromHashCode(imi,
-                        subtypeHashCode);
-                if (subtypeIndex >= 0) {
-                    return imi.getSubtypeAt(subtypeIndex);
-                }
-            }
-
-            // If there are no selected subtypes, the framework will try to find the most applicable
-            // subtype from explicitly or implicitly enabled subtypes.
-            final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
-                    getEnabledInputMethodSubtypeListLocked(imi, true);
-            // If there is only one explicitly or implicitly enabled subtype, just returns it.
-            if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) {
-                return null;
-            }
-            if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
-                return explicitlyOrImplicitlyEnabledSubtypes.get(0);
-            }
-            final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString();
-            final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
-                    explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD,
-                    locale, true);
-            if (subtype != null) {
-                return subtype;
-            }
-            return SubtypeUtils.findLastResortApplicableSubtypeLocked(
-                    explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
-        }
-
-        boolean setAdditionalInputMethodSubtypes(@NonNull String imeId,
-                @NonNull ArrayList<InputMethodSubtype> subtypes,
-                @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
-                @NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
-            final InputMethodInfo imi = mMethodMap.get(imeId);
-            if (imi == null) {
-                return false;
-            }
-            if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
-                    imi.getPackageName())) {
-                return false;
-            }
-
-            if (subtypes.isEmpty()) {
-                additionalSubtypeMap.remove(imi.getId());
-            } else {
-                additionalSubtypeMap.put(imi.getId(), subtypes);
-            }
-            AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId());
-            return true;
-        }
-
-        boolean setEnabledInputMethodSubtypes(@NonNull String imeId,
-                @NonNull int[] subtypeHashCodes) {
-            final InputMethodInfo imi = mMethodMap.get(imeId);
-            if (imi == null) {
-                return false;
-            }
-
-            final IntArray validSubtypeHashCodes = new IntArray(subtypeHashCodes.length);
-            for (int subtypeHashCode : subtypeHashCodes) {
-                if (subtypeHashCode == NOT_A_SUBTYPE_ID) {
-                    continue;  // NOT_A_SUBTYPE_ID must not be saved
-                }
-                if (!SubtypeUtils.isValidSubtypeId(imi, subtypeHashCode)) {
-                    continue;  // this subtype does not exist in InputMethodInfo.
-                }
-                if (validSubtypeHashCodes.indexOf(subtypeHashCode) >= 0) {
-                    continue;  // The entry is already added.  No need to add anymore.
-                }
-                validSubtypeHashCodes.add(subtypeHashCode);
-            }
-
-            final String originalEnabledImesString = getEnabledInputMethodsStr();
-            final String updatedEnabledImesString = updateEnabledImeString(
-                    originalEnabledImesString, imi.getId(), validSubtypeHashCodes);
-            if (TextUtils.equals(originalEnabledImesString, updatedEnabledImesString)) {
-                return false;
-            }
-
-            putEnabledInputMethodsStr(updatedEnabledImesString);
-            return true;
-        }
-
-        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-        static String updateEnabledImeString(@NonNull String enabledImesString,
-                @NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes) {
-            final TextUtils.SimpleStringSplitter imeSplitter =
-                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
-            final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
-                    new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
-
-            final StringBuilder sb = new StringBuilder();
-
-            imeSplitter.setString(enabledImesString);
-            boolean needsImeSeparator = false;
-            while (imeSplitter.hasNext()) {
-                final String nextImsStr = imeSplitter.next();
-                imeSubtypeSplitter.setString(nextImsStr);
-                if (imeSubtypeSplitter.hasNext()) {
-                    if (needsImeSeparator) {
-                        sb.append(INPUT_METHOD_SEPARATOR);
-                    }
-                    if (TextUtils.equals(imeId, imeSubtypeSplitter.next())) {
-                        sb.append(imeId);
-                        for (int i = 0; i < enabledSubtypeHashCodes.size(); ++i) {
-                            sb.append(INPUT_METHOD_SUBTYPE_SEPARATOR);
-                            sb.append(enabledSubtypeHashCodes.get(i));
-                        }
-                    } else {
-                        sb.append(nextImsStr);
-                    }
-                    needsImeSeparator = true;
-                }
-            }
-            return sb.toString();
-        }
-
-        public void dumpLocked(final Printer pw, final String prefix) {
-            pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
-        }
-    }
-
     static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion,
             @StartInputFlags int startInputFlags) {
         if (targetSdkVersion < Build.VERSION_CODES.P) {
diff --git a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java b/services/core/java/com/android/server/media/MediaFeatureFlagManager.java
deleted file mode 100644
index f90f64a..0000000
--- a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.media;
-
-import android.annotation.StringDef;
-import android.app.ActivityThread;
-import android.app.Application;
-import android.provider.DeviceConfig;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/* package */ class MediaFeatureFlagManager {
-
-    /**
-     * Namespace for media better together features.
-     */
-    private static final String NAMESPACE_MEDIA_BETTER_TOGETHER = "media_better_together";
-
-    @StringDef(
-            prefix = "FEATURE_",
-            value = {
-                FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE
-            })
-    @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
-    @Retention(RetentionPolicy.SOURCE)
-    /* package */ @interface MediaFeatureFlag {}
-
-    /**
-     * Whether to use IMPORTANCE_FOREGROUND (i.e. 100) or IMPORTANCE_FOREGROUND_SERVICE (i.e. 125)
-     * as the minimum package importance for scanning.
-     */
-    /* package */ static final @MediaFeatureFlag String
-            FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE = "scanning_package_minimum_importance";
-
-    private static final MediaFeatureFlagManager sInstance = new MediaFeatureFlagManager();
-
-    private MediaFeatureFlagManager() {
-        // Empty to prevent instantiation.
-    }
-
-    /* package */ static MediaFeatureFlagManager getInstance() {
-        return sInstance;
-    }
-
-    /**
-     * Returns a boolean value from {@link DeviceConfig} from the system_time namespace, or
-     * {@code defaultValue} if there is no explicit value set.
-     */
-    public boolean getBoolean(@MediaFeatureFlag String key, boolean defaultValue) {
-        return DeviceConfig.getBoolean(NAMESPACE_MEDIA_BETTER_TOGETHER, key, defaultValue);
-    }
-
-    /**
-     * Returns an int value from {@link DeviceConfig} from the system_time namespace, or {@code
-     * defaultValue} if there is no explicit value set.
-     */
-    public int getInt(@MediaFeatureFlag String key, int defaultValue) {
-        return DeviceConfig.getInt(NAMESPACE_MEDIA_BETTER_TOGETHER, key, defaultValue);
-    }
-
-    /**
-     * Adds a listener to react for changes in media feature flags values. Future calls to this
-     * method with the same listener will replace the old namespace and executor.
-     *
-     * @param onPropertiesChangedListener The listener to add.
-     */
-    public void addOnPropertiesChangedListener(
-            DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener) {
-        Application currentApplication = ActivityThread.currentApplication();
-        if (currentApplication != null) {
-            DeviceConfig.addOnPropertiesChangedListener(
-                    NAMESPACE_MEDIA_BETTER_TOGETHER,
-                    currentApplication.getMainExecutor(),
-                    onPropertiesChangedListener);
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index e048522..28a1c7a 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -16,7 +16,7 @@
 
 package com.android.server.media;
 
-import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
 import static android.content.Intent.ACTION_SCREEN_OFF;
 import static android.content.Intent.ACTION_SCREEN_ON;
 import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
@@ -24,7 +24,6 @@
 import static android.media.MediaRouter2Utils.getProviderId;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-import static com.android.server.media.MediaFeatureFlagManager.FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -55,7 +54,6 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.provider.DeviceConfig;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -97,11 +95,7 @@
     //       in MediaRouter2, remove this constant and replace the usages with the real request IDs.
     private static final long DUMMY_REQUEST_ID = -1;
 
-    private static int sPackageImportanceForScanning =
-            MediaFeatureFlagManager.getInstance()
-                    .getInt(
-                            FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE,
-                            IMPORTANCE_FOREGROUND_SERVICE);
+    private static final int REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING = IMPORTANCE_FOREGROUND;
 
     /**
      * Contains the list of bluetooth permissions that are required to do system routing.
@@ -159,7 +153,7 @@
         mContext = context;
         mActivityManager = mContext.getSystemService(ActivityManager.class);
         mActivityManager.addOnUidImportanceListener(mOnUidImportanceListener,
-                sPackageImportanceForScanning);
+                REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING);
         mPowerManager = mContext.getSystemService(PowerManager.class);
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
 
@@ -171,9 +165,6 @@
         }
 
         mContext.getPackageManager().addOnPermissionsChangeListener(this::onPermissionsChanged);
-
-        MediaFeatureFlagManager.getInstance()
-                .addOnPropertiesChangedListener(this::onDeviceConfigChange);
     }
 
     /**
@@ -1443,8 +1434,13 @@
             return;
         }
 
-        Slog.i(TAG, TextUtils.formatSimple(
-                "startScan | manager: %d", managerRecord.mManagerId));
+        Slog.i(
+                TAG,
+                TextUtils.formatSimple(
+                        "startScan | manager: %d, ownerPackageName: %s, targetPackageName: %s",
+                        managerRecord.mManagerId,
+                        managerRecord.mOwnerPackageName,
+                        managerRecord.mTargetPackageName));
 
         managerRecord.startScan();
     }
@@ -1457,8 +1453,13 @@
             return;
         }
 
-        Slog.i(TAG, TextUtils.formatSimple(
-                "stopScan | manager: %d", managerRecord.mManagerId));
+        Slog.i(
+                TAG,
+                TextUtils.formatSimple(
+                        "stopScan | manager: %d, ownerPackageName: %s, targetPackageName: %s",
+                        managerRecord.mManagerId,
+                        managerRecord.mOwnerPackageName,
+                        managerRecord.mTargetPackageName));
 
         managerRecord.stopScan();
     }
@@ -1725,13 +1726,6 @@
 
     // End of locked methods that are used by both MediaRouter2 and MediaRouter2Manager.
 
-    private void onDeviceConfigChange(@NonNull DeviceConfig.Properties properties) {
-        sPackageImportanceForScanning =
-                properties.getInt(
-                        /* name */ FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE,
-                        /* defaultValue */ IMPORTANCE_FOREGROUND_SERVICE);
-    }
-
     static long toUniqueRequestId(int requesterId, int originalRequestId) {
         return ((long) requesterId << 32) | originalRequestId;
     }
@@ -3170,7 +3164,7 @@
                             record ->
                                     service.mActivityManager.getPackageImportance(
                                                     record.mPackageName)
-                                            <= sPackageImportanceForScanning)
+                                            <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING)
                     .collect(Collectors.toList());
         }
 
@@ -3187,7 +3181,7 @@
                                     manager.mIsScanning
                                             && service.mActivityManager.getPackageImportance(
                                                             manager.mOwnerPackageName)
-                                                    <= sPackageImportanceForScanning);
+                                                    <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING);
         }
 
         private MediaRoute2Provider findProvider(@Nullable String providerId) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9ddc362..c9f45e5 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -12103,6 +12103,7 @@
                 return true;
             }
 
+            long token = Binder.clearCallingIdentity();
             try {
                 if (mPackageManager.checkUidPermission(RECEIVE_SENSITIVE_NOTIFICATIONS, uid)
                         == PERMISSION_GRANTED || mPackageManagerInternal.isPlatformSigned(pkg)) {
@@ -12129,6 +12130,8 @@
                 }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to check trusted status of listener", e);
+            } finally {
+                Binder.restoreCallingIdentity(token);
             }
             return false;
         }
diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java
index 91df04c..37b263c 100644
--- a/services/core/java/com/android/server/notification/ZenAdapters.java
+++ b/services/core/java/com/android/server/notification/ZenAdapters.java
@@ -59,9 +59,7 @@
         }
 
         if (Flags.modesApi()) {
-            zenPolicyBuilder.allowChannels(
-                    policy.allowPriorityChannels()
-                            ? ZenPolicy.CHANNEL_TYPE_PRIORITY : ZenPolicy.CHANNEL_TYPE_NONE);
+            zenPolicyBuilder.allowPriorityChannels(policy.allowPriorityChannels());
         }
 
         return zenPolicyBuilder.build();
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index 0145577..a90efe6 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -18,6 +18,8 @@
 
 import static android.app.NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND;
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_PRIORITY;
+import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_NONE;
 import static android.service.notification.NotificationServiceProto.RULE_TYPE_AUTOMATIC;
 import static android.service.notification.NotificationServiceProto.RULE_TYPE_MANUAL;
 import static android.service.notification.NotificationServiceProto.RULE_TYPE_UNKNOWN;
@@ -551,8 +553,8 @@
                 if (Flags.modesApi()) {
                     proto.write(DNDPolicyProto.ALLOW_CHANNELS,
                             mNewPolicy.allowPriorityChannels()
-                                    ? ZenPolicy.CHANNEL_TYPE_PRIORITY
-                                    : ZenPolicy.CHANNEL_TYPE_NONE);
+                                    ? CHANNEL_POLICY_PRIORITY
+                                    : CHANNEL_POLICY_NONE);
                 }
             } else {
                 Log.wtf(TAG, "attempted to write zen mode log event with null policy");
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index afbf08d..c7f5703 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -1131,7 +1131,7 @@
                     != newPolicy.getPriorityConversationSenders()) {
                 userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS;
             }
-            if (oldPolicy.getAllowedChannels() != newPolicy.getAllowedChannels()) {
+            if (oldPolicy.getPriorityChannels() != newPolicy.getPriorityChannels()) {
                 userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS;
             }
             if (oldPolicy.getPriorityCategoryReminders()
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 659c36c..5d71439e 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -276,7 +276,8 @@
      * Returns list of {@code packageName} of apks inside the given apex.
      * @param apexPackageName Package name of the apk container of apex
      */
-    abstract List<String> getApksInApex(String apexPackageName);
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public abstract List<String> getApksInApex(String apexPackageName);
 
     /**
      * Returns the apex module name for the given package name, if the package is an APEX. Otherwise
@@ -751,8 +752,9 @@
             }
         }
 
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
         @Override
-        List<String> getApksInApex(String apexPackageName) {
+        public List<String> getApksInApex(String apexPackageName) {
             synchronized (mLock) {
                 Preconditions.checkState(mPackageNameToApexModuleName != null,
                         "APEX packages have not been scanned");
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index 7f0aadc..c110fb6 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -75,11 +75,9 @@
     private static final int MSG_PACKAGE_ADDED = 1;
     private static final int MSG_PACKAGE_REMOVED = 2;
 
-    private final Context mContext;
     private final BinderService mBinderService;
     private final PackageManager mPackageManager;
     private final PackageManagerInternal mPackageManagerInternal;
-    private final UsageStatsManagerInternal mUsageStatsManagerInternal;
     private final PermissionManagerServiceInternal mPermissionManager;
     private final Handler mHandler;
     private final File mDiskFile;
@@ -99,14 +97,14 @@
     @VisibleForTesting
     BackgroundInstallControlService(@NonNull Injector injector) {
         super(injector.getContext());
-        mContext = injector.getContext();
         mPackageManager = injector.getPackageManager();
         mPackageManagerInternal = injector.getPackageManagerInternal();
         mPermissionManager = injector.getPermissionManager();
         mHandler = new EventHandler(injector.getLooper(), this);
         mDiskFile = injector.getDiskFile();
-        mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal();
-        mUsageStatsManagerInternal.registerListener(
+        UsageStatsManagerInternal usageStatsManagerInternal =
+                injector.getUsageStatsManagerInternal();
+        usageStatsManagerInternal.registerListener(
                 (userId, event) ->
                         mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED,
                                 userId,
diff --git a/services/core/java/com/android/server/pm/DataLoaderManagerService.java b/services/core/java/com/android/server/pm/DataLoaderManagerService.java
index 888e1c2..c25cea6 100644
--- a/services/core/java/com/android/server/pm/DataLoaderManagerService.java
+++ b/services/core/java/com/android/server/pm/DataLoaderManagerService.java
@@ -47,7 +47,6 @@
 public class DataLoaderManagerService extends SystemService {
     private static final String TAG = "DataLoaderManager";
     private final Context mContext;
-    private final HandlerThread mThread;
     private final Handler mHandler;
     private final DataLoaderManagerBinderService mBinderService;
     private final SparseArray<DataLoaderServiceConnection> mServiceConnections =
@@ -57,10 +56,10 @@
         super(context);
         mContext = context;
 
-        mThread = new HandlerThread(TAG);
-        mThread.start();
+        HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
 
-        mHandler = new Handler(mThread.getLooper());
+        mHandler = new Handler(thread.getLooper());
 
         mBinderService = new DataLoaderManagerBinderService();
     }
diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java
index e3bbd2d..f987d4a 100644
--- a/services/core/java/com/android/server/pm/IPackageManagerBase.java
+++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java
@@ -107,9 +107,6 @@
     @Nullable
     private final ComponentName mInstantAppResolverSettingsComponent;
 
-    @NonNull
-    private final String mRequiredSupplementalProcessPackage;
-
     @Nullable
     private final String mServicesExtensionPackageName;
 
@@ -125,7 +122,6 @@
             @NonNull PackageInstallerService installerService,
             @NonNull PackageProperty packageProperty, @NonNull ComponentName resolveComponentName,
             @Nullable ComponentName instantAppResolverSettingsComponent,
-            @NonNull String requiredSupplementalProcessPackage,
             @Nullable String servicesExtensionPackageName,
             @Nullable String sharedSystemSharedLibraryPackageName) {
         mService = service;
@@ -140,7 +136,6 @@
         mPackageProperty = packageProperty;
         mResolveComponentName = resolveComponentName;
         mInstantAppResolverSettingsComponent = instantAppResolverSettingsComponent;
-        mRequiredSupplementalProcessPackage = requiredSupplementalProcessPackage;
         mServicesExtensionPackageName = servicesExtensionPackageName;
         mSharedSystemSharedLibraryPackageName = sharedSystemSharedLibraryPackageName;
     }
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index d5471cb0..34903d1 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -1183,8 +1183,7 @@
      * Returns an auth token for the provided writable FD.
      *
      * @param authFd a file descriptor to proof that the caller can write to the file.
-     * @param appUid uid of the calling app.
-     * @param userId id of the user whose app file to enable fs-verity.
+     * @param uid uid of the calling app.
      *
      * @return authToken, or null if a remote call shouldn't be continued. See {@link
      * #checkBeforeRemote}.
@@ -1192,13 +1191,12 @@
      * @throws InstallerException if the remote call failed.
      */
     public IInstalld.IFsveritySetupAuthToken createFsveritySetupAuthToken(
-            ParcelFileDescriptor authFd, int appUid, @UserIdInt int userId)
-            throws InstallerException {
+            ParcelFileDescriptor authFd, int uid) throws InstallerException {
         if (!checkBeforeRemote()) {
             return null;
         }
         try {
-            return mInstalld.createFsveritySetupAuthToken(authFd, appUid, userId);
+            return mInstalld.createFsveritySetupAuthToken(authFd, uid);
         } catch (Exception e) {
             throw InstallerException.from(e);
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 5225529..c5b5a76 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4659,8 +4659,7 @@
                     mPreferredActivityHelper, mResolveIntentHelper, mDomainVerificationManager,
                     mDomainVerificationConnection, mInstallerService, mPackageProperty,
                     mResolveComponentName, mInstantAppResolverSettingsComponent,
-                    mRequiredSdkSandboxPackage, mServicesExtensionPackageName,
-                    mSharedSystemSharedLibraryPackageName);
+                    mServicesExtensionPackageName, mSharedSystemSharedLibraryPackageName);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/ProtectedPackages.java b/services/core/java/com/android/server/pm/ProtectedPackages.java
index 98533725..524252c 100644
--- a/services/core/java/com/android/server/pm/ProtectedPackages.java
+++ b/services/core/java/com/android/server/pm/ProtectedPackages.java
@@ -57,11 +57,8 @@
     @GuardedBy("this")
     private final SparseArray<Set<String>> mOwnerProtectedPackages = new SparseArray<>();
 
-    private final Context mContext;
-
     public ProtectedPackages(Context context) {
-        mContext = context;
-        mDeviceProvisioningPackage = mContext.getResources().getString(
+        mDeviceProvisioningPackage = context.getResources().getString(
                 R.string.config_deviceProvisioningPackage);
     }
 
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 7bd6a43..3e3b72c 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -67,7 +67,6 @@
     private final PackageManagerService mPm;
     private final IncrementalManager mIncrementalManager;
     private final Installer mInstaller;
-    private final UserManagerInternal mUserManagerInternal;
     private final PermissionManagerServiceInternal mPermissionManager;
     private final SharedLibrariesImpl mSharedLibraries;
     private final AppDataHelper mAppDataHelper;
@@ -79,7 +78,6 @@
         mPm = pm;
         mIncrementalManager = mPm.mInjector.getIncrementalManager();
         mInstaller = mPm.mInjector.getInstaller();
-        mUserManagerInternal = mPm.mInjector.getUserManagerInternal();
         mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal();
         mSharedLibraries = mPm.mInjector.getSharedLibrariesImpl();
         mAppDataHelper = appDataHelper;
diff --git a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java
index 7f6f684..aa52522 100644
--- a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java
@@ -31,7 +31,6 @@
  * The access to it must be guarded with the shortcut manager lock.
  */
 public class ShortcutNonPersistentUser {
-    private final ShortcutService mService;
 
     private final int mUserId;
 
@@ -49,8 +48,7 @@
      */
     private final ArraySet<String> mHostPackageSet = new ArraySet<>();
 
-    public ShortcutNonPersistentUser(ShortcutService service, int userId) {
-        mService = service;
+    public ShortcutNonPersistentUser(int userId) {
         mUserId = userId;
     }
 
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 446c629..96c205c 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -1378,7 +1378,7 @@
     ShortcutNonPersistentUser getNonPersistentUserLocked(@UserIdInt int userId) {
         ShortcutNonPersistentUser ret = mShortcutNonPersistentUsers.get(userId);
         if (ret == null) {
-            ret = new ShortcutNonPersistentUser(this, userId);
+            ret = new ShortcutNonPersistentUser(userId);
             mShortcutNonPersistentUsers.put(userId, ret);
         }
         return ret;
diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java
index 4c42c2d..1d41401 100644
--- a/services/core/java/com/android/server/pm/UserDataPreparer.java
+++ b/services/core/java/com/android/server/pm/UserDataPreparer.java
@@ -141,7 +141,7 @@
                 // If internal storage of the system user fails to prepare on first boot, then
                 // things are *really* broken, so we might as well reboot to recovery right away.
                 try {
-                    Log.wtf(TAG, "prepareUserData failed for user " + userId, e);
+                    Log.e(TAG, "prepareUserData failed for user " + userId, e);
                     if (isNewUser && userId == UserHandle.USER_SYSTEM && volumeUuid == null) {
                         RecoverySystem.rebootPromptAndWipeUserData(mContext,
                                 "failed to prepare internal storage for system user");
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index c6435ae..f0ff85d 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -356,6 +356,11 @@
         if (verifierUser == UserHandle.ALL) {
             verifierUser = UserHandle.of(mPm.mUserManager.getCurrentUserId());
         }
+        // TODO(b/300965895): Remove when inconsistencies loading classpaths from apex for
+        // user > 1 are fixed.
+        if (pkgLite.isSdkLibrary) {
+            verifierUser = UserHandle.SYSTEM;
+        }
         final int verifierUserId = verifierUser.getIdentifier();
 
         List<String> requiredVerifierPackages = new ArrayList<>(
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 5d710d2..40f2264 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -29,6 +29,7 @@
 import static android.app.AppOpsManager.OP_BLUETOOTH_CONNECT;
 import static android.content.pm.ApplicationInfo.AUTO_REVOKE_DISALLOWED;
 import static android.content.pm.ApplicationInfo.AUTO_REVOKE_DISCOURAGED;
+import static android.permission.flags.Flags.serverSideAttributionRegistration;
 
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 
@@ -76,7 +77,6 @@
 import com.android.internal.util.function.QuadFunction;
 import com.android.internal.util.function.TriFunction;
 import com.android.server.LocalServices;
-import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.UserManagerService;
 import com.android.server.pm.permission.PermissionManagerServiceInternal.HotwordDetectionServiceProvider;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -113,9 +113,6 @@
     /** Internal connection to the package manager */
     private final PackageManagerInternal mPackageManagerInt;
 
-    /** Internal connection to the user manager */
-    private final UserManagerInternal mUserManagerInt;
-
     /** Map of OneTimePermissionUserManagers keyed by userId */
     @GuardedBy("mLock")
     @NonNull
@@ -147,7 +144,6 @@
 
         mContext = context;
         mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class);
-        mUserManagerInt = LocalServices.getService(UserManagerInternal.class);
         mAppOpsManager = context.getSystemService(AppOpsManager.class);
 
         mAttributionSourceRegistry = new AttributionSourceRegistry(context);
@@ -439,10 +435,27 @@
         }
     }
 
+    /**
+     * Reference propagation over binder is affected by the ownership of the object. So if 
+     * the token is owned by client, references to the token on client side won't be 
+     * propagated to the server and the token may still be garbage collected on server side. 
+     * But if the token is owned by server, references to the token on client side will now 
+     * be propagated to the server since it's a foreign object to the client, and that will 
+     * keep the token referenced on the server side as long as the client is alive and 
+     * holding it.
+     */
     @Override
-    public void registerAttributionSource(@NonNull AttributionSourceState source) {
-        mAttributionSourceRegistry
-                .registerAttributionSource(new AttributionSource(source));
+    public IBinder registerAttributionSource(@NonNull AttributionSourceState source) {
+        if (serverSideAttributionRegistration()) {
+            Binder token = new Binder();
+            mAttributionSourceRegistry
+                    .registerAttributionSource(new AttributionSource(source).withToken(token));
+            return token;
+        } else {
+            mAttributionSourceRegistry
+                    .registerAttributionSource(new AttributionSource(source));
+            return source.token;
+        }
     }
 
     @Override
@@ -1080,12 +1093,10 @@
         private static final AtomicInteger sAttributionChainIds = new AtomicInteger(0);
 
         private final @NonNull Context mContext;
-        private final @NonNull AppOpsManager mAppOpsManager;
         private final @NonNull PermissionManagerServiceInternal mPermissionManagerServiceInternal;
 
         PermissionCheckerService(@NonNull Context context) {
             mContext = context;
-            mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
             mPermissionManagerServiceInternal =
                     LocalServices.getService(PermissionManagerServiceInternal.class);
         }
@@ -1218,7 +1229,6 @@
                 @Nullable String message, boolean forDataDelivery, boolean startDataDelivery,
                 boolean fromDatasource, int attributedOp) {
             PermissionInfo permissionInfo = sPlatformPermissions.get(permission);
-
             if (permissionInfo == null) {
                 try {
                     permissionInfo = context.getPackageManager().getPermissionInfo(permission, 0);
@@ -1346,8 +1356,8 @@
 
                 // If the call is from a datasource we need to vet only the chain before it. This
                 // way we can avoid the datasource creating an attribution context for every call.
-                if (!(fromDatasource && current.equals(attributionSource))
-                        && next != null && !current.isTrusted(context)) {
+                boolean isDatasource = fromDatasource && current.equals(attributionSource);
+                if (!isDatasource && next != null && !current.isTrusted(context)) {
                     return PermissionChecker.PERMISSION_HARD_DENIED;
                 }
 
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 3afba39..6a57362 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -279,7 +279,6 @@
     @NonNull
     private final int[] mGlobalGids;
 
-    private final HandlerThread mHandlerThread;
     private final Handler mHandler;
     private final Context mContext;
     private final MetricsLogger mMetricsLogger = new MetricsLogger();
@@ -432,10 +431,10 @@
             }
         }
 
-        mHandlerThread = new ServiceThread(TAG,
+        HandlerThread handlerThread = new ServiceThread(TAG,
                 Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
         Watchdog.getInstance().addThread(mHandler);
 
         SystemConfig systemConfig = SystemConfig.getInstance();
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 2128c991..e226953 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -139,6 +139,7 @@
 import com.android.server.power.batterysaver.BatterySaverPolicy;
 import com.android.server.power.batterysaver.BatterySaverStateMachine;
 import com.android.server.power.batterysaver.BatterySavingStats;
+import com.android.server.power.feature.PowerManagerFlags;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -329,6 +330,8 @@
     // True if battery saver is supported on this device.
     private final boolean mBatterySaverSupported;
 
+    private final PowerManagerFlags mFeatureFlags;
+
     private boolean mDisableScreenWakeLocksWhileCached;
 
     private LightsManager mLightsManager;
@@ -1079,6 +1082,10 @@
         DeviceConfigParameterProvider createDeviceConfigParameterProvider() {
             return new DeviceConfigParameterProvider(DeviceConfigInterface.REAL);
         }
+
+        PowerManagerFlags getFlags() {
+            return new PowerManagerFlags();
+        }
     }
 
     /** Interface for checking an app op permission */
@@ -1145,6 +1152,7 @@
         mNativeWrapper = injector.createNativeWrapper();
         mSystemProperties = injector.createSystemPropertiesWrapper();
         mClock = injector.createClock();
+        mFeatureFlags = injector.getFlags();
         mInjector = injector;
 
         mHandlerThread = new ServiceThread(TAG,
@@ -4802,6 +4810,7 @@
         mAmbientDisplaySuppressionController.dump(pw);
 
         mLowPowerStandbyController.dump(pw);
+        mFeatureFlags.dump(pw);
     }
 
     private void dumpProto(FileDescriptor fd) {
diff --git a/services/core/java/com/android/server/power/feature/Android.bp b/services/core/java/com/android/server/power/feature/Android.bp
new file mode 100644
index 0000000..2295b41
--- /dev/null
+++ b/services/core/java/com/android/server/power/feature/Android.bp
@@ -0,0 +1,7 @@
+aconfig_declarations {
+    name: "power_flags",
+    package: "com.android.server.power.feature.flags",
+    srcs: [
+        "*.aconfig",
+    ],
+}
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
new file mode 100644
index 0000000..a5a7069
--- /dev/null
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.feature;
+
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.server.power.feature.flags.Flags;
+
+import java.io.PrintWriter;
+import java.util.function.Supplier;
+
+/**
+ * Utility class to read the flags used in the power manager server.
+ */
+public class PowerManagerFlags {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "PowerManagerFlags";
+
+    private final FlagState mEarlyScreenTimeoutDetectorFlagState = new FlagState(
+            Flags.FLAG_ENABLE_EARLY_SCREEN_TIMEOUT_DETECTOR,
+            Flags::enableEarlyScreenTimeoutDetector);
+
+    /** Returns whether early-screen-timeout-detector is enabled on not. */
+    public boolean isEarlyScreenTimeoutDetectorEnabled() {
+        return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
+    }
+
+    /**
+     * dumps all flagstates
+     * @param pw printWriter
+     */
+    public void dump(PrintWriter pw) {
+        pw.println("PowerManagerFlags:");
+        pw.println(" " + mEarlyScreenTimeoutDetectorFlagState);
+    }
+
+    private static class FlagState {
+
+        private final String mName;
+
+        private final Supplier<Boolean> mFlagFunction;
+        private boolean mEnabledSet;
+        private boolean mEnabled;
+
+        private FlagState(String name, Supplier<Boolean> flagFunction) {
+            mName = name;
+            mFlagFunction = flagFunction;
+        }
+
+        private boolean isEnabled() {
+            if (mEnabledSet) {
+                if (DEBUG) {
+                    Slog.d(TAG, mName + ": mEnabled. Recall = " + mEnabled);
+                }
+                return mEnabled;
+            }
+            mEnabled = mFlagFunction.get();
+            if (DEBUG) {
+                Slog.d(TAG, mName + ": mEnabled. Flag value = " + mEnabled);
+            }
+            mEnabledSet = true;
+            return mEnabled;
+        }
+
+        @Override
+        public String toString() {
+            // remove com.android.server.power.feature.flags. from the beginning of the name.
+            // align all isEnabled() values.
+            // Adjust lengths if we end up with longer names
+            final int nameLength = mName.length();
+            return TextUtils.substring(mName,  39, nameLength) + ": "
+                    + TextUtils.formatSimple("%" + (91 - nameLength) + "s%s", " " , isEnabled())
+                    + " (def:" + mFlagFunction.get() + ")";
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
new file mode 100644
index 0000000..c8c16db
--- /dev/null
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -0,0 +1,11 @@
+package: "com.android.server.power.feature.flags"
+
+# Important: Flags must be accessed through PowerManagerFlags.
+
+flag {
+    name: "enable_early_screen_timeout_detector"
+    namespace: "power_manager"
+    description: "Feature flag for Early Screen Timeout detector"
+    bug: "309861917"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java
index a49df50..bb4876b 100644
--- a/services/core/java/com/android/server/security/FileIntegrityService.java
+++ b/services/core/java/com/android/server/security/FileIntegrityService.java
@@ -157,7 +157,7 @@
             Objects.requireNonNull(authFd);
             try {
                 var authToken = getStorageManagerInternal().createFsveritySetupAuthToken(authFd,
-                        Binder.getCallingUid(), Binder.getCallingUserHandle().getIdentifier());
+                        Binder.getCallingUid());
                 // fs-verity setup requires no writable fd to the file. Release the dup now that
                 // it's passed.
                 authFd.close();
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index 0467d0c..7ab075e 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -38,7 +38,16 @@
 import android.media.tv.BroadcastInfoResponse;
 import android.media.tv.TvRecordingInfo;
 import android.media.tv.TvTrackInfo;
+import android.media.tv.ad.ITvAdClient;
 import android.media.tv.ad.ITvAdManager;
+import android.media.tv.ad.ITvAdManagerCallback;
+import android.media.tv.ad.ITvAdService;
+import android.media.tv.ad.ITvAdServiceCallback;
+import android.media.tv.ad.ITvAdSession;
+import android.media.tv.ad.ITvAdSessionCallback;
+import android.media.tv.ad.TvAdService;
+import android.media.tv.ad.TvAdServiceInfo;
+import android.media.tv.flags.Flags;
 import android.media.tv.interactive.AppLinkInfo;
 import android.media.tv.interactive.ITvInteractiveAppClient;
 import android.media.tv.interactive.ITvInteractiveAppManager;
@@ -110,6 +119,8 @@
     @GuardedBy("mLock")
     private boolean mGetServiceListCalled = false;
     @GuardedBy("mLock")
+    private boolean mGetAdServiceListCalled = false;
+    @GuardedBy("mLock")
     private boolean mGetAppLinkInfoListCalled = false;
 
     private final UserManager mUserManager;
@@ -256,6 +267,141 @@
     }
 
     @GuardedBy("mLock")
+    private void buildTvAdServiceListLocked(int userId, String[] updatedPackages) {
+        if (!Flags.enableAdServiceFw()) {
+            return;
+        }
+        UserState userState = getOrCreateUserStateLocked(userId);
+        userState.mPackageSet.clear();
+
+        if (DEBUG) {
+            Slogf.d(TAG, "buildTvAdServiceListLocked");
+        }
+        PackageManager pm = mContext.getPackageManager();
+        List<ResolveInfo> services = pm.queryIntentServicesAsUser(
+                new Intent(TvAdService.SERVICE_INTERFACE),
+                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
+                userId);
+        List<TvAdServiceInfo> serviceList = new ArrayList<>();
+
+        for (ResolveInfo ri : services) {
+            ServiceInfo si = ri.serviceInfo;
+            if (!android.Manifest.permission.BIND_TV_AD_SERVICE.equals(si.permission)) {
+                Slog.w(TAG, "Skipping TV AD service " + si.name
+                        + ": it does not require the permission "
+                        + android.Manifest.permission.BIND_TV_AD_SERVICE);
+                continue;
+            }
+
+            ComponentName component = new ComponentName(si.packageName, si.name);
+            try {
+                TvAdServiceInfo info = new TvAdServiceInfo(mContext, component);
+                serviceList.add(info);
+            } catch (Exception e) {
+                Slogf.e(TAG, "failed to load TV AD service " + si.name, e);
+                continue;
+            }
+            userState.mPackageSet.add(si.packageName);
+        }
+
+        // sort the service list by service id
+        Collections.sort(serviceList, Comparator.comparing(TvAdServiceInfo::getId));
+        Map<String, TvAdServiceState> adServiceMap = new HashMap<>();
+        for (TvAdServiceInfo info : serviceList) {
+            String serviceId = info.getId();
+            if (DEBUG) {
+                Slogf.d(TAG, "add " + serviceId);
+            }
+            TvAdServiceState adServiceState = userState.mAdServiceMap.get(serviceId);
+            if (adServiceState == null) {
+                adServiceState = new TvAdServiceState();
+            }
+            adServiceState.mInfo = info;
+            adServiceState.mUid = getAdServiceUid(info);
+            adServiceState.mComponentName = info.getComponent();
+            adServiceMap.put(serviceId, adServiceState);
+        }
+
+        for (String serviceId : adServiceMap.keySet()) {
+            if (!userState.mAdServiceMap.containsKey(serviceId)) {
+                notifyAdServiceAddedLocked(userState, serviceId);
+            } else if (updatedPackages != null) {
+                // Notify the package updates
+                ComponentName component = adServiceMap.get(serviceId).mInfo.getComponent();
+                for (String updatedPackage : updatedPackages) {
+                    if (component.getPackageName().equals(updatedPackage)) {
+                        updateAdServiceConnectionLocked(component, userId);
+                        notifyAdServiceUpdatedLocked(userState, serviceId);
+                        break;
+                    }
+                }
+            }
+        }
+
+        for (String serviceId : userState.mAdServiceMap.keySet()) {
+            if (!adServiceMap.containsKey(serviceId)) {
+                TvAdServiceInfo info = userState.mAdServiceMap.get(serviceId).mInfo;
+                AdServiceState serviceState = userState.mAdServiceStateMap.get(info.getComponent());
+                if (serviceState != null) {
+                    abortPendingCreateAdSessionRequestsLocked(serviceState, serviceId, userId);
+                }
+                notifyAdServiceRemovedLocked(userState, serviceId);
+            }
+        }
+
+        userState.mIAppMap.clear();
+        userState.mAdServiceMap = adServiceMap;
+    }
+
+    @GuardedBy("mLock")
+    private void notifyAdServiceAddedLocked(UserState userState, String serviceId) {
+        if (DEBUG) {
+            Slog.d(TAG, "notifyAdServiceAddedLocked(serviceId=" + serviceId + ")");
+        }
+        int n = userState.mAdCallbacks.beginBroadcast();
+        for (int i = 0; i < n; ++i) {
+            try {
+                userState.mAdCallbacks.getBroadcastItem(i).onAdServiceAdded(serviceId);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "failed to report added AD service to callback", e);
+            }
+        }
+        userState.mAdCallbacks.finishBroadcast();
+    }
+
+    @GuardedBy("mLock")
+    private void notifyAdServiceRemovedLocked(UserState userState, String serviceId) {
+        if (DEBUG) {
+            Slog.d(TAG, "notifyAdServiceRemovedLocked(serviceId=" + serviceId + ")");
+        }
+        int n = userState.mAdCallbacks.beginBroadcast();
+        for (int i = 0; i < n; ++i) {
+            try {
+                userState.mAdCallbacks.getBroadcastItem(i).onAdServiceRemoved(serviceId);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "failed to report removed AD service to callback", e);
+            }
+        }
+        userState.mAdCallbacks.finishBroadcast();
+    }
+
+    @GuardedBy("mLock")
+    private void notifyAdServiceUpdatedLocked(UserState userState, String serviceId) {
+        if (DEBUG) {
+            Slog.d(TAG, "notifyAdServiceUpdatedLocked(serviceId=" + serviceId + ")");
+        }
+        int n = userState.mAdCallbacks.beginBroadcast();
+        for (int i = 0; i < n; ++i) {
+            try {
+                userState.mAdCallbacks.getBroadcastItem(i).onAdServiceUpdated(serviceId);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "failed to report updated AD service to callback", e);
+            }
+        }
+        userState.mAdCallbacks.finishBroadcast();
+    }
+
+    @GuardedBy("mLock")
     private void notifyInteractiveAppServiceAddedLocked(UserState userState, String iAppServiceId) {
         if (DEBUG) {
             Slog.d(TAG, "notifyInteractiveAppServiceAddedLocked(iAppServiceId="
@@ -340,6 +486,16 @@
         }
     }
 
+    private int getAdServiceUid(TvAdServiceInfo info) {
+        try {
+            return getContext().getPackageManager().getApplicationInfo(
+                    info.getServiceInfo().packageName, 0).uid;
+        } catch (PackageManager.NameNotFoundException e) {
+            Slogf.w(TAG, "Unable to get UID for  " + info, e);
+            return Process.INVALID_UID;
+        }
+    }
+
     @Override
     public void onStart() {
         if (DEBUG) {
@@ -357,6 +513,7 @@
             synchronized (mLock) {
                 buildTvInteractiveAppServiceListLocked(mCurrentUserId, null);
                 buildAppLinkInfoLocked(mCurrentUserId);
+                buildTvAdServiceListLocked(mCurrentUserId, null);
             }
         }
     }
@@ -372,6 +529,14 @@
                     }
                 }
             }
+            private void buildTvAdServiceList(String[] packages) {
+                int userId = getChangingUserId();
+                synchronized (mLock) {
+                    if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
+                        buildTvAdServiceListLocked(userId, packages);
+                    }
+                }
+            }
 
             @Override
             public void onPackageUpdateFinished(String packageName, int uid) {
@@ -379,6 +544,7 @@
                 // This callback is invoked when the TV interactive App service is reinstalled.
                 // In this case, isReplacing() always returns true.
                 buildTvInteractiveAppServiceList(new String[] { packageName });
+                buildTvAdServiceList(new String[] { packageName });
             }
 
             @Override
@@ -390,6 +556,7 @@
                 // available.
                 if (isReplacing()) {
                     buildTvInteractiveAppServiceList(packages);
+                    buildTvAdServiceList(packages);
                 }
             }
 
@@ -403,6 +570,7 @@
                 }
                 if (isReplacing()) {
                     buildTvInteractiveAppServiceList(packages);
+                    buildTvAdServiceList(packages);
                 }
             }
 
@@ -418,6 +586,7 @@
                     return;
                 }
                 buildTvInteractiveAppServiceList(null);
+                buildTvAdServiceList(null);
             }
 
             @Override
@@ -476,6 +645,7 @@
             mCurrentUserId = userId;
             buildTvInteractiveAppServiceListLocked(userId, null);
             buildAppLinkInfoLocked(userId);
+            buildTvAdServiceListLocked(userId, null);
         }
     }
 
@@ -562,6 +732,7 @@
         mRunningProfiles.add(userId);
         buildTvInteractiveAppServiceListLocked(userId, null);
         buildAppLinkInfoLocked(userId);
+        buildTvAdServiceListLocked(userId, null);
     }
 
     @GuardedBy("mLock")
@@ -619,7 +790,19 @@
                 Slog.e(TAG, "error in onSessionReleased", e);
             }
         }
-        removeSessionStateLocked(state.mSessionToken, state.mUserId);
+        removeAdSessionStateLocked(state.mSessionToken, state.mUserId);
+    }
+
+    @GuardedBy("mLock")
+    private void clearAdSessionAndNotifyClientLocked(AdSessionState state) {
+        if (state.mClient != null) {
+            try {
+                state.mClient.onSessionReleased(state.mSeq);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "error in onSessionReleased", e);
+            }
+        }
+        removeAdSessionStateLocked(state.mSessionToken, state.mUserId);
     }
 
     private int resolveCallingUserId(int callingPid, int callingUid, int requestedUserId,
@@ -655,6 +838,44 @@
     }
 
     @GuardedBy("mLock")
+    private AdSessionState getAdSessionStateLocked(
+            IBinder sessionToken, int callingUid, int userId) {
+        UserState userState = getOrCreateUserStateLocked(userId);
+        return getAdSessionStateLocked(sessionToken, callingUid, userState);
+    }
+
+    @GuardedBy("mLock")
+    private AdSessionState getAdSessionStateLocked(IBinder sessionToken, int callingUid,
+            UserState userState) {
+        AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken);
+        if (sessionState == null) {
+            throw new SessionNotFoundException("Session state not found for token " + sessionToken);
+        }
+        // Only the application that requested this session or the system can access it.
+        if (callingUid != Process.SYSTEM_UID && callingUid != sessionState.mCallingUid) {
+            throw new SecurityException("Illegal access to the session with token " + sessionToken
+                    + " from uid " + callingUid);
+        }
+        return sessionState;
+    }
+
+    @GuardedBy("mLock")
+    private ITvAdSession getAdSessionLocked(
+            IBinder sessionToken, int callingUid, int userId) {
+        return getAdSessionLocked(getAdSessionStateLocked(sessionToken, callingUid, userId));
+    }
+
+    @GuardedBy("mLock")
+    private ITvAdSession getAdSessionLocked(AdSessionState sessionState) {
+        ITvAdSession session = sessionState.mSession;
+        if (session == null) {
+            throw new IllegalStateException("Session not yet created for token "
+                    + sessionState.mSessionToken);
+        }
+        return session;
+    }
+
+    @GuardedBy("mLock")
     private SessionState getSessionStateLocked(IBinder sessionToken, int callingUid, int userId) {
         UserState userState = getOrCreateUserStateLocked(userId);
         return getSessionStateLocked(sessionToken, callingUid, userState);
@@ -691,10 +912,200 @@
         return session;
     }
     private final class TvAdBinderService extends ITvAdManager.Stub {
+
+        @Override
+        public List<TvAdServiceInfo> getTvAdServiceList(int userId) {
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+                    Binder.getCallingUid(), userId, "getTvAdServiceList");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    if (!mGetAdServiceListCalled) {
+                        buildTvAdServiceListLocked(userId, null);
+                        mGetAdServiceListCalled = true;
+                    }
+                    UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+                    List<TvAdServiceInfo> adServiceList = new ArrayList<>();
+                    for (TvAdServiceState state : userState.mAdServiceMap.values()) {
+                        adServiceList.add(state.mInfo);
+                    }
+                    return adServiceList;
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void createSession(final ITvAdClient client, final String serviceId, String type,
+                int seq, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int callingPid = Binder.getCallingPid();
+            final int resolvedUserId = resolveCallingUserId(callingPid, callingUid,
+                    userId, "createSession");
+            final long identity = Binder.clearCallingIdentity();
+
+            try {
+                synchronized (mLock) {
+                    if (userId != mCurrentUserId && !mRunningProfiles.contains(userId)) {
+                        // Only current user and its running profiles can create sessions.
+                        // Let the client get onConnectionFailed callback for this case.
+                        sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq);
+                        return;
+                    }
+                    UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+                    TvAdServiceState adState = userState.mAdMap.get(serviceId);
+                    if (adState == null) {
+                        Slogf.w(TAG, "Failed to find state for serviceId=" + serviceId);
+                        sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq);
+                        return;
+                    }
+                    AdServiceState serviceState =
+                            userState.mAdServiceStateMap.get(adState.mComponentName);
+                    if (serviceState == null) {
+                        int tasUid = PackageManager.getApplicationInfoAsUserCached(
+                                adState.mComponentName.getPackageName(), 0, resolvedUserId).uid;
+                        serviceState = new AdServiceState(
+                                adState.mComponentName, serviceId, resolvedUserId);
+                        userState.mAdServiceStateMap.put(adState.mComponentName, serviceState);
+                    }
+                    // Send a null token immediately while reconnecting.
+                    if (serviceState.mReconnecting) {
+                        sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq);
+                        return;
+                    }
+
+                    // Create a new session token and a session state.
+                    IBinder sessionToken = new Binder();
+                    AdSessionState sessionState = new AdSessionState(sessionToken, serviceId, type,
+                            adState.mComponentName, client, seq, callingUid,
+                            callingPid, resolvedUserId);
+
+                    // Add them to the global session state map of the current user.
+                    userState.mAdSessionStateMap.put(sessionToken, sessionState);
+
+                    // Also, add them to the session state map of the current service.
+                    serviceState.mSessionTokens.add(sessionToken);
+
+                    if (serviceState.mService != null) {
+                        if (!createAdSessionInternalLocked(serviceState.mService, sessionToken,
+                                resolvedUserId)) {
+                            removeAdSessionStateLocked(sessionToken, resolvedUserId);
+                        }
+                    } else {
+                        updateAdServiceConnectionLocked(adState.mComponentName, resolvedUserId);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void releaseSession(IBinder sessionToken, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "releaseSession(sessionToken=" + sessionToken + ")");
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "releaseSession");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    releaseSessionLocked(sessionToken, callingUid, resolvedUserId);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void setSurface(IBinder sessionToken, Surface surface, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "setSurface");
+            AdSessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+                                resolvedUserId);
+                        getAdSessionLocked(sessionState).setSurface(surface);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in setSurface", e);
+                    }
+                }
+            } finally {
+                if (surface != null) {
+                    // surface is not used in TvInteractiveAppManagerService.
+                    surface.release();
+                }
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void dispatchSurfaceChanged(IBinder sessionToken, int format, int width,
+                int height, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "dispatchSurfaceChanged");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        AdSessionState sessionState = getAdSessionStateLocked(
+                                sessionToken, callingUid, resolvedUserId);
+                        getAdSessionLocked(sessionState).dispatchSurfaceChanged(format, width,
+                                height);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in dispatchSurfaceChanged", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
         @Override
         public void startAdService(IBinder sessionToken, int userId) {
         }
 
+        @Override
+        public void registerCallback(final ITvAdManagerCallback callback, int userId) {
+            int callingPid = Binder.getCallingPid();
+            int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+                    "registerCallback");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    final UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+                    if (!userState.mAdCallbacks.register(callback)) {
+                        Slog.e(TAG, "client process has already died");
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void unregisterCallback(ITvAdManagerCallback callback, int userId) {
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+                    Binder.getCallingUid(), userId, "unregisterCallback");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+                    userState.mAdCallbacks.unregister(callback);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
     }
 
     private final class BinderService extends ITvInteractiveAppManager.Stub {
@@ -927,7 +1338,7 @@
             final long identity = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    releaseSessionLocked(sessionToken, callingUid, resolvedUserId);
+                    releaseAdSessionLocked(sessionToken, callingUid, resolvedUserId);
                 }
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -1471,6 +1882,32 @@
         }
 
         @Override
+        public void sendSelectedTrackInfo(IBinder sessionToken, List<TvTrackInfo> tracks,
+                int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "sendSelectedTrackInfo(tracks=%s)", tracks.toString());
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "sendSelectedTrackInfo");
+            SessionState sessionState = null;
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        sessionState = getSessionStateLocked(sessionToken, callingUid,
+                                resolvedUserId);
+                        getSessionLocked(sessionState).sendSelectedTrackInfo(tracks);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in sendSelectedTrackInfo", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void sendCurrentTvInputId(IBinder sessionToken, String inputId, int userId) {
             if (DEBUG) {
                 Slogf.d(TAG, "sendCurrentTvInputId(inputId=%s)", inputId);
@@ -2134,6 +2571,17 @@
     }
 
     @GuardedBy("mLock")
+    private void sendAdSessionTokenToClientLocked(
+            ITvAdClient client, String serviceId, IBinder sessionToken,
+            InputChannel channel, int seq) {
+        try {
+            client.onSessionCreated(serviceId, sessionToken, channel, seq);
+        } catch (RemoteException e) {
+            Slogf.e(TAG, "error in onSessionCreated", e);
+        }
+    }
+
+    @GuardedBy("mLock")
     private boolean createSessionInternalLocked(
             ITvInteractiveAppService service, IBinder sessionToken, int userId) {
         UserState userState = getOrCreateUserStateLocked(userId);
@@ -2163,6 +2611,58 @@
     }
 
     @GuardedBy("mLock")
+    private boolean createAdSessionInternalLocked(
+            ITvAdService service, IBinder sessionToken, int userId) {
+        UserState userState = getOrCreateUserStateLocked(userId);
+        AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken);
+        if (DEBUG) {
+            Slogf.d(TAG, "createAdSessionInternalLocked(iAppServiceId="
+                    + sessionState.mAdServiceId + ")");
+        }
+        InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString());
+
+        // Set up a callback to send the session token.
+        ITvAdSessionCallback callback = new AdSessionCallback(sessionState, channels);
+
+        boolean created = true;
+        // Create a session. When failed, send a null token immediately.
+        try {
+            service.createSession(
+                    channels[1], callback, sessionState.mAdServiceId, sessionState.mType);
+        } catch (RemoteException e) {
+            Slogf.e(TAG, "error in createSession", e);
+            sendAdSessionTokenToClientLocked(sessionState.mClient, sessionState.mAdServiceId, null,
+                    null, sessionState.mSeq);
+            created = false;
+        }
+        channels[1].dispose();
+        return created;
+    }
+
+    @GuardedBy("mLock")
+    @Nullable
+    private AdSessionState releaseAdSessionLocked(
+            IBinder sessionToken, int callingUid, int userId) {
+        AdSessionState sessionState = null;
+        try {
+            sessionState = getAdSessionStateLocked(sessionToken, callingUid, userId);
+            UserState userState = getOrCreateUserStateLocked(userId);
+            if (sessionState.mSession != null) {
+                sessionState.mSession.asBinder().unlinkToDeath(sessionState, 0);
+                sessionState.mSession.release();
+            }
+        } catch (RemoteException | SessionNotFoundException e) {
+            Slogf.e(TAG, "error in releaseSession", e);
+        } finally {
+            if (sessionState != null) {
+                sessionState.mSession = null;
+            }
+        }
+        removeAdSessionStateLocked(sessionToken, userId);
+        return sessionState;
+    }
+
+    @GuardedBy("mLock")
     @Nullable
     private SessionState releaseSessionLocked(IBinder sessionToken, int callingUid, int userId) {
         SessionState sessionState = null;
@@ -2215,6 +2715,36 @@
     }
 
     @GuardedBy("mLock")
+    private void removeAdSessionStateLocked(IBinder sessionToken, int userId) {
+        UserState userState = getOrCreateUserStateLocked(userId);
+
+        // Remove the session state from the global session state map of the current user.
+        AdSessionState sessionState = userState.mAdSessionStateMap.remove(sessionToken);
+
+        if (sessionState == null) {
+            Slogf.e(TAG, "sessionState null, no more remove session action!");
+            return;
+        }
+
+        // Also remove the session token from the session token list of the current client and
+        // service.
+        ClientState clientState = userState.mClientStateMap.get(sessionState.mClient.asBinder());
+        if (clientState != null) {
+            clientState.mSessionTokens.remove(sessionToken);
+            if (clientState.isEmpty()) {
+                userState.mClientStateMap.remove(sessionState.mClient.asBinder());
+                sessionState.mClient.asBinder().unlinkToDeath(clientState, 0);
+            }
+        }
+
+        AdServiceState serviceState = userState.mAdServiceStateMap.get(sessionState.mComponent);
+        if (serviceState != null) {
+            serviceState.mSessionTokens.remove(sessionToken);
+        }
+        updateAdServiceConnectionLocked(sessionState.mComponent, userId);
+    }
+
+    @GuardedBy("mLock")
     private void abortPendingCreateSessionRequestsLocked(ServiceState serviceState,
             String iAppServiceId, int userId) {
         // Let clients know the create session requests are failed.
@@ -2237,6 +2767,28 @@
     }
 
     @GuardedBy("mLock")
+    private void abortPendingCreateAdSessionRequestsLocked(AdServiceState serviceState,
+            String serviceId, int userId) {
+        // Let clients know the create session requests are failed.
+        UserState userState = getOrCreateUserStateLocked(userId);
+        List<AdSessionState> sessionsToAbort = new ArrayList<>();
+        for (IBinder sessionToken : serviceState.mSessionTokens) {
+            AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken);
+            if (sessionState.mSession == null
+                    && (serviceState == null
+                    || sessionState.mAdServiceId.equals(serviceId))) {
+                sessionsToAbort.add(sessionState);
+            }
+        }
+        for (AdSessionState sessionState : sessionsToAbort) {
+            removeAdSessionStateLocked(sessionState.mSessionToken, sessionState.mUserId);
+            sendAdSessionTokenToClientLocked(sessionState.mClient,
+                    sessionState.mAdServiceId, null, null, sessionState.mSeq);
+        }
+        updateAdServiceConnectionLocked(serviceState.mComponent, userId);
+    }
+
+    @GuardedBy("mLock")
     private void updateServiceConnectionLocked(ComponentName component, int userId) {
         UserState userState = getOrCreateUserStateLocked(userId);
         ServiceState serviceState = userState.mServiceStateMap.get(component);
@@ -2284,10 +2836,64 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private void updateAdServiceConnectionLocked(ComponentName component, int userId) {
+        UserState userState = getOrCreateUserStateLocked(userId);
+        AdServiceState serviceState = userState.mAdServiceStateMap.get(component);
+        if (serviceState == null) {
+            return;
+        }
+        if (serviceState.mReconnecting) {
+            if (!serviceState.mSessionTokens.isEmpty()) {
+                // wait until all the sessions are removed.
+                return;
+            }
+            serviceState.mReconnecting = false;
+        }
+
+        boolean shouldBind = (!serviceState.mSessionTokens.isEmpty())
+                || (!serviceState.mPendingAppLinkCommand.isEmpty());
+
+        if (serviceState.mService == null && shouldBind) {
+            // This means that the service is not yet connected but its state indicates that we
+            // have pending requests. Then, connect the service.
+            if (serviceState.mBound) {
+                // We have already bound to the service so we don't try to bind again until after we
+                // unbind later on.
+                return;
+            }
+            if (DEBUG) {
+                Slogf.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")");
+            }
+
+            Intent i = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component);
+            serviceState.mBound = mContext.bindServiceAsUser(
+                    i, serviceState.mConnection,
+                    Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
+                    new UserHandle(userId));
+        } else if (serviceState.mService != null && !shouldBind) {
+            // This means that the service is already connected but its state indicates that we have
+            // nothing to do with it. Then, disconnect the service.
+            if (DEBUG) {
+                Slogf.d(TAG, "unbindService(service=" + component + ")");
+            }
+            mContext.unbindService(serviceState.mConnection);
+            userState.mAdServiceStateMap.remove(component);
+        }
+    }
+
     private static final class UserState {
         private final int mUserId;
+        // A mapping from the TV AD service ID to its TvAdServiceState.
+        private Map<String, TvAdServiceState> mAdMap = new HashMap<>();
+        // A mapping from the name of a TV Interactive App service to its state.
+        private final Map<ComponentName, AdServiceState> mAdServiceStateMap = new HashMap<>();
+        // A mapping from the token of a TV Interactive App session to its state.
+        private final Map<IBinder, AdSessionState> mAdSessionStateMap = new HashMap<>();
         // A mapping from the TV Interactive App ID to its TvInteractiveAppState.
         private Map<String, TvInteractiveAppState> mIAppMap = new HashMap<>();
+        // A mapping from the TV AD service ID to its TvAdServiceState.
+        private Map<String, TvAdServiceState> mAdServiceMap = new HashMap<>();
         // A mapping from the token of a client to its state.
         private final Map<IBinder, ClientState> mClientStateMap = new HashMap<>();
         // A mapping from the name of a TV Interactive App service to its state.
@@ -2299,6 +2905,8 @@
         private final Set<String> mPackageSet = new HashSet<>();
         // A list of all app link infos.
         private final List<AppLinkInfo> mAppLinkInfoList = new ArrayList<>();
+        private final RemoteCallbackList<ITvAdManagerCallback> mAdCallbacks =
+                new RemoteCallbackList<>();
 
         // A list of callbacks.
         private final RemoteCallbackList<ITvInteractiveAppManagerCallback> mCallbacks =
@@ -2317,7 +2925,16 @@
         private int mIAppNumber;
     }
 
+    private static final class TvAdServiceState {
+        private String mAdServiceId;
+        private ComponentName mComponentName;
+        private TvAdServiceInfo mInfo;
+        private int mUid;
+        private int mAdNumber;
+    }
+
     private final class SessionState implements IBinder.DeathRecipient {
+        // TODO: rename SessionState and reorganize classes / methods of this file
         private final IBinder mSessionToken;
         private ITvInteractiveAppSession mSession;
         private final String mIAppServiceId;
@@ -2359,6 +2976,49 @@
         }
     }
 
+    private final class AdSessionState implements IBinder.DeathRecipient {
+        private final IBinder mSessionToken;
+        private ITvAdSession mSession;
+        private final String mAdServiceId;
+
+        private final String mType;
+        private final ITvAdClient mClient;
+        private final int mSeq;
+        private final ComponentName mComponent;
+
+        // The UID of the application that created the session.
+        // The application is usually the TV app.
+        private final int mCallingUid;
+
+        // The PID of the application that created the session.
+        // The application is usually the TV app.
+        private final int mCallingPid;
+
+        private final int mUserId;
+
+        private AdSessionState(IBinder sessionToken, String serviceId, String type,
+                ComponentName componentName, ITvAdClient client, int seq,
+                int callingUid, int callingPid, int userId) {
+            mSessionToken = sessionToken;
+            mAdServiceId = serviceId;
+            mType = type;
+            mComponent = componentName;
+            mClient = client;
+            mSeq = seq;
+            mCallingUid = callingUid;
+            mCallingPid = callingPid;
+            mUserId = userId;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mLock) {
+                mSession = null;
+                clearAdSessionAndNotifyClientLocked(this);
+            }
+        }
+    }
+
     private final class ClientState implements IBinder.DeathRecipient {
         private final List<IBinder> mSessionTokens = new ArrayList<>();
 
@@ -2429,6 +3089,29 @@
         }
     }
 
+    private final class AdServiceState {
+        private final List<IBinder> mSessionTokens = new ArrayList<>();
+        private final ServiceConnection mConnection;
+        private final ComponentName mComponent;
+        private final String mAdServiceId;
+        private final List<Bundle> mPendingAppLinkCommand = new ArrayList<>();
+
+        private ITvAdService mService;
+        private AdServiceCallback mCallback;
+        private boolean mBound;
+        private boolean mReconnecting;
+
+        private AdServiceState(ComponentName component, String tasId, int userId) {
+            mComponent = component;
+            mConnection = new AdServiceConnection(component, userId);
+            mAdServiceId = tasId;
+        }
+
+        private void addPendingAppLinkCommand(Bundle command) {
+            mPendingAppLinkCommand.add(command);
+        }
+    }
+
     private final class InteractiveAppServiceConnection implements ServiceConnection {
         private final ComponentName mComponent;
         private final int mUserId;
@@ -2542,6 +3225,98 @@
         }
     }
 
+    private final class AdServiceConnection implements ServiceConnection {
+        private final ComponentName mComponent;
+        private final int mUserId;
+
+        private AdServiceConnection(ComponentName component, int userId) {
+            mComponent = component;
+            mUserId = userId;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName component, IBinder service) {
+            if (DEBUG) {
+                Slogf.d(TAG, "onServiceConnected(component=" + component + ")");
+            }
+            synchronized (mLock) {
+                UserState userState = getUserStateLocked(mUserId);
+                if (userState == null) {
+                    // The user was removed while connecting.
+                    mContext.unbindService(this);
+                    return;
+                }
+                AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent);
+                serviceState.mService = ITvAdService.Stub.asInterface(service);
+
+                // Register a callback, if we need to.
+                if (serviceState.mCallback == null) {
+                    serviceState.mCallback = new AdServiceCallback(mComponent, mUserId);
+                    try {
+                        serviceState.mService.registerCallback(serviceState.mCallback);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "error in registerCallback", e);
+                    }
+                }
+
+                if (!serviceState.mPendingAppLinkCommand.isEmpty()) {
+                    for (Iterator<Bundle> it = serviceState.mPendingAppLinkCommand.iterator();
+                            it.hasNext(); ) {
+                        Bundle command = it.next();
+                        final long identity = Binder.clearCallingIdentity();
+                        try {
+                            serviceState.mService.sendAppLinkCommand(command);
+                            it.remove();
+                        } catch (RemoteException e) {
+                            Slogf.e(TAG, "error in sendAppLinkCommand(" + command
+                                    + ") when onServiceConnected", e);
+                        } finally {
+                            Binder.restoreCallingIdentity(identity);
+                        }
+                    }
+                }
+
+                List<IBinder> tokensToBeRemoved = new ArrayList<>();
+
+                // And create sessions, if any.
+                for (IBinder sessionToken : serviceState.mSessionTokens) {
+                    if (!createAdSessionInternalLocked(
+                            serviceState.mService, sessionToken, mUserId)) {
+                        tokensToBeRemoved.add(sessionToken);
+                    }
+                }
+
+                for (IBinder sessionToken : tokensToBeRemoved) {
+                    removeAdSessionStateLocked(sessionToken, mUserId);
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName component) {
+            if (DEBUG) {
+                Slogf.d(TAG, "onServiceDisconnected(component=" + component + ")");
+            }
+            if (!mComponent.equals(component)) {
+                throw new IllegalArgumentException("Mismatched ComponentName: "
+                        + mComponent + " (expected), " + component + " (actual).");
+            }
+            synchronized (mLock) {
+                UserState userState = getOrCreateUserStateLocked(mUserId);
+                AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent);
+                if (serviceState != null) {
+                    serviceState.mReconnecting = true;
+                    serviceState.mBound = false;
+                    serviceState.mService = null;
+                    serviceState.mCallback = null;
+
+                    abortPendingCreateAdSessionRequestsLocked(serviceState, null, mUserId);
+                }
+            }
+        }
+    }
+
+
     private final class ServiceCallback extends ITvInteractiveAppServiceCallback.Stub {
         private final ComponentName mComponent;
         private final int mUserId;
@@ -2567,6 +3342,17 @@
         }
     }
 
+
+    private final class AdServiceCallback extends ITvAdServiceCallback.Stub {
+        private final ComponentName mComponent;
+        private final int mUserId;
+
+        AdServiceCallback(ComponentName component, int userId) {
+            mComponent = component;
+            mUserId = userId;
+        }
+    }
+
     private final class SessionCallback extends ITvInteractiveAppSessionCallback.Stub {
         private final SessionState mSessionState;
         private final InputChannel[] mInputChannels;
@@ -2798,6 +3584,23 @@
         }
 
         @Override
+        public void onRequestSelectedTrackInfo() {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestSelectedTrackInfo");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestSelectedTrackInfo(mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestSelectedTrackInfo", e);
+                }
+            }
+        }
+
+        @Override
         public void onRequestCurrentTvInputId() {
             synchronized (mLock) {
                 if (DEBUG) {
@@ -3110,6 +3913,85 @@
         }
     }
 
+    private final class AdSessionCallback extends ITvAdSessionCallback.Stub {
+        private final AdSessionState mSessionState;
+        private final InputChannel[] mInputChannels;
+
+        AdSessionCallback(AdSessionState sessionState, InputChannel[] channels) {
+            mSessionState = sessionState;
+            mInputChannels = channels;
+        }
+
+        @Override
+        public void onSessionCreated(ITvAdSession session) {
+            if (DEBUG) {
+                Slogf.d(TAG, "onSessionCreated(adServiceId="
+                        + mSessionState.mAdServiceId + ")");
+            }
+            synchronized (mLock) {
+                mSessionState.mSession = session;
+                if (session != null && addAdSessionTokenToClientStateLocked(session)) {
+                    sendAdSessionTokenToClientLocked(
+                            mSessionState.mClient,
+                            mSessionState.mAdServiceId,
+                            mSessionState.mSessionToken,
+                            mInputChannels[0],
+                            mSessionState.mSeq);
+                } else {
+                    removeAdSessionStateLocked(mSessionState.mSessionToken, mSessionState.mUserId);
+                    sendAdSessionTokenToClientLocked(mSessionState.mClient,
+                            mSessionState.mAdServiceId, null, null, mSessionState.mSeq);
+                }
+                mInputChannels[0].dispose();
+            }
+        }
+
+        @Override
+        public void onLayoutSurface(int left, int top, int right, int bottom) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top
+                            + ", right=" + right + ", bottom=" + bottom + ",)");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onLayoutSurface(left, top, right, bottom,
+                            mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onLayoutSurface", e);
+                }
+            }
+        }
+
+        @GuardedBy("mLock")
+        private boolean addAdSessionTokenToClientStateLocked(ITvAdSession session) {
+            try {
+                session.asBinder().linkToDeath(mSessionState, 0);
+            } catch (RemoteException e) {
+                Slogf.e(TAG, "session process has already died", e);
+                return false;
+            }
+
+            IBinder clientToken = mSessionState.mClient.asBinder();
+            UserState userState = getOrCreateUserStateLocked(mSessionState.mUserId);
+            ClientState clientState = userState.mClientStateMap.get(clientToken);
+            if (clientState == null) {
+                clientState = new ClientState(clientToken, mSessionState.mUserId);
+                try {
+                    clientToken.linkToDeath(clientState, 0);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "client process has already died", e);
+                    return false;
+                }
+                userState.mClientStateMap.put(clientToken, clientState);
+            }
+            clientState.mSessionTokens.add(mSessionState.mSessionToken);
+            return true;
+        }
+    }
+
     private static class SessionNotFoundException extends IllegalArgumentException {
         SessionNotFoundException(String name) {
             super(name);
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
index e54b40e..03c75e0 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
@@ -37,7 +37,17 @@
     void revokeUriPermission(String targetPackage, int callingUid,
             GrantUri grantUri, final int modeFlags);
 
-    boolean checkUriPermission(GrantUri grantUri, int uid, final int modeFlags);
+    /**
+     * Check if the uid has permission to the URI in grantUri.
+     *
+     * @param isFullAccessForContentUri If true, the URI has to be a content URI
+     *                                  and the method will consider full access.
+     *                                  Otherwise, the method will only consider
+     *                                  URI grants.
+     */
+    boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags,
+            boolean isFullAccessForContentUri);
+
     int checkGrantUriPermission(
             int callingUid, String targetPkg, Uri uri, int modeFlags, int userId);
 
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index e501b9d..ce2cbed 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
@@ -25,6 +25,7 @@
 import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
 import static android.content.pm.PackageManager.MATCH_ANY_USER;
 import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
@@ -1103,7 +1104,8 @@
      */
     private int checkGrantUriPermissionUnlocked(int callingUid, String targetPkg, GrantUri grantUri,
             int modeFlags, int lastTargetUid) {
-        if (!Intent.isAccessUriMode(modeFlags)) {
+        if (!isContentUriWithAccessModeFlags(grantUri, modeFlags,
+                /* logAction */ "grant URI permission")) {
             return -1;
         }
 
@@ -1111,12 +1113,6 @@
             if (DEBUG) Slog.v(TAG, "Checking grant " + targetPkg + " permission to " + grantUri);
         }
 
-        // If this is not a content: uri, we can't do anything with it.
-        if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
-            if (DEBUG) Slog.v(TAG, "Can't grant URI permission for non-content URI: " + grantUri);
-            return -1;
-        }
-
         // Bail early if system is trying to hand out permissions directly; it
         // must always grant permissions on behalf of someone explicit.
         final int callingAppId = UserHandle.getAppId(callingUid);
@@ -1137,7 +1133,7 @@
 
         final String authority = grantUri.uri.getAuthority();
         final ProviderInfo pi = getProviderInfo(authority, grantUri.sourceUserId,
-                MATCH_DEBUG_TRIAGED_MISSING, callingUid);
+                MATCH_DIRECT_BOOT_AUTO, callingUid);
         if (pi == null) {
             Slog.w(TAG, "No content provider found for permission check: " +
                     grantUri.uri.toSafeString());
@@ -1285,6 +1281,65 @@
         return targetUid;
     }
 
+    private boolean isContentUriWithAccessModeFlags(GrantUri grantUri, int modeFlags,
+            String logAction) {
+        if (!Intent.isAccessUriMode(modeFlags)) {
+            if (DEBUG) Slog.v(TAG, "Mode flags are not access URI mode flags: " + modeFlags);
+            return false;
+        }
+
+        if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
+            if (DEBUG) {
+                Slog.v(TAG, "Can't " + logAction + " on non-content URI: " + grantUri);
+            }
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Check if the uid has permission to the content URI in grantUri. */
+    private boolean checkContentUriPermissionFullUnlocked(GrantUri grantUri, int uid,
+            int modeFlags) {
+        if (uid < 0) {
+            throw new IllegalArgumentException("Uid must be positive for the content URI "
+                    + "permission check of " + grantUri.uri.toSafeString());
+        }
+
+        if (!isContentUriWithAccessModeFlags(grantUri, modeFlags,
+                /* logAction */ "check content URI permission")) {
+            throw new IllegalArgumentException("The URI must be a content URI and the mode "
+                    + "flags must be at least read and/or write for the content URI permission "
+                    + "check of " + grantUri.uri.toSafeString());
+        }
+
+        final int appId = UserHandle.getAppId(uid);
+        if ((appId == SYSTEM_UID) || (appId == ROOT_UID)) {
+            return true;
+        }
+
+        // Retrieve the URI's content provider
+        final String authority = grantUri.uri.getAuthority();
+        ProviderInfo pi = getProviderInfo(authority, grantUri.sourceUserId, MATCH_DIRECT_BOOT_AUTO,
+                uid);
+
+        if (pi == null) {
+            Slog.w(TAG, "No content provider found for content URI permission check: "
+                    + grantUri.uri.toSafeString());
+            return false;
+        }
+
+        // Check if it has general permission to the URI's content provider
+        if (checkHoldingPermissionsUnlocked(pi, grantUri, uid, modeFlags)) {
+            return true;
+        }
+
+        // Check if it has explicitly granted permissions to the URI
+        synchronized (mLock) {
+            return checkUriPermissionLocked(grantUri, uid, modeFlags);
+        }
+    }
+
     /**
      * @param userId The userId in which the uri is to be resolved.
      */
@@ -1482,7 +1537,12 @@
         }
 
         @Override
-        public boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags) {
+        public boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags,
+                boolean isFullAccessForContentUri) {
+            if (isFullAccessForContentUri) {
+                return UriGrantsManagerService.this.checkContentUriPermissionFullUnlocked(grantUri,
+                        uid, modeFlags);
+            }
             synchronized (mLock) {
                 return UriGrantsManagerService.this.checkUriPermissionLocked(grantUri, uid,
                         modeFlags);
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index fcc0de1..3094b18 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -1910,6 +1910,12 @@
                 // Transforms do not need to be persisted; the IkeSession will keep them alive
                 mIpSecManager.applyTunnelModeTransform(tunnelIface, direction, transform);
 
+                if (direction == IpSecManager.DIRECTION_IN
+                        && mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                        && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+                    mUnderlyingNetworkController.updateInboundTransform(mUnderlying, transform);
+                }
+
                 // For inbound transforms, additionally allow forwarded traffic to bridge to DUN (as
                 // needed)
                 final Set<Integer> exposedCaps = mConnectionConfig.getAllExposedCapabilities();
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 48df44b..3f8d39e 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -30,6 +30,7 @@
 import android.annotation.Nullable;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IpSecTransform;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -52,6 +53,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
 import com.android.server.vcn.util.LogUtils;
 
 import java.util.ArrayList;
@@ -201,6 +203,14 @@
         NetworkCallback oldWifiExitRssiThresholdCallback = mWifiExitRssiThresholdCallback;
         List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks);
         mCellBringupCallbacks.clear();
+
+        if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+            for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
+                evaluator.close();
+            }
+        }
+
         mUnderlyingNetworkRecords.clear();
 
         // Register new callbacks. Make-before-break; always register new callbacks before removal
@@ -417,11 +427,42 @@
         if (oldSnapshot
                 .getAllSubIdsInGroup(mSubscriptionGroup)
                 .equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) {
+
+            if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                    && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+                reevaluateNetworks();
+            }
             return;
         }
         registerOrUpdateNetworkRequests();
     }
 
+    /**
+     * Pass the IpSecTransform of the VCN to UnderlyingNetworkController for metric monitoring
+     *
+     * <p>Caller MUST call it when IpSecTransforms have been created for VCN creation or migration
+     */
+    public void updateInboundTransform(
+            @NonNull UnderlyingNetworkRecord currentNetwork, @NonNull IpSecTransform transform) {
+        if (!mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                || !mVcnContext.isFlagIpSecTransformStateEnabled()) {
+            logWtf("#updateInboundTransform: unexpected call; flags missing");
+            return;
+        }
+
+        Objects.requireNonNull(currentNetwork, "currentNetwork is null");
+        Objects.requireNonNull(transform, "transform is null");
+
+        if (mCurrentRecord == null
+                || mRouteSelectionCallback == null
+                || !Objects.equals(currentNetwork.network, mCurrentRecord.network)) {
+            // The caller (VcnGatewayConnection) is out-of-dated. Ignore this call.
+            return;
+        }
+
+        mUnderlyingNetworkRecords.get(mCurrentRecord.network).setInboundTransform(transform);
+    }
+
     /** Tears down this Tracker, and releases all underlying network requests. */
     public void teardown() {
         mVcnContext.ensureRunningOnLooperThread();
@@ -438,7 +479,7 @@
 
     private TreeSet<UnderlyingNetworkEvaluator> getSortedUnderlyingNetworks() {
         TreeSet<UnderlyingNetworkEvaluator> sorted =
-                new TreeSet<>(UnderlyingNetworkEvaluator.getComparator());
+                new TreeSet<>(UnderlyingNetworkEvaluator.getComparator(mVcnContext));
 
         for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
             if (evaluator.getPriorityClass() != NetworkPriorityClassifier.PRIORITY_INVALID) {
@@ -525,11 +566,17 @@
                             mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
                             mSubscriptionGroup,
                             mLastSnapshot,
-                            mCarrierConfig));
+                            mCarrierConfig,
+                            new NetworkEvaluatorCallbackImpl()));
         }
 
         @Override
         public void onLost(@NonNull Network network) {
+            if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                    && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+                mUnderlyingNetworkRecords.get(network).close();
+            }
+
             mUnderlyingNetworkRecords.remove(network);
 
             reevaluateNetworks();
@@ -598,6 +645,21 @@
         }
     }
 
+    @VisibleForTesting
+    class NetworkEvaluatorCallbackImpl implements NetworkEvaluatorCallback {
+        @Override
+        public void onEvaluationResultChanged() {
+            if (!mVcnContext.isFlagNetworkMetricMonitorEnabled()
+                    || !mVcnContext.isFlagIpSecTransformStateEnabled()) {
+                logWtf("#onEvaluationResultChanged: unexpected call; flags missing");
+                return;
+            }
+
+            mVcnContext.ensureRunningOnLooperThread();
+            reevaluateNetworks();
+        }
+    }
+
     private String getLogPrefix() {
         return "("
                 + LogUtils.getHashedSubscriptionGroup(mSubscriptionGroup)
@@ -690,21 +752,22 @@
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     public static class Dependencies {
-        /** Construct a new UnderlyingNetworkEvaluator */
         public UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator(
                 @NonNull VcnContext vcnContext,
                 @NonNull Network network,
                 @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
                 @NonNull ParcelUuid subscriptionGroup,
                 @NonNull TelephonySubscriptionSnapshot lastSnapshot,
-                @Nullable PersistableBundleWrapper carrierConfig) {
+                @Nullable PersistableBundleWrapper carrierConfig,
+                @NonNull NetworkEvaluatorCallback evaluatorCallback) {
             return new UnderlyingNetworkEvaluator(
                     vcnContext,
                     network,
                     underlyingNetworkTemplates,
                     subscriptionGroup,
                     lastSnapshot,
-                    carrierConfig);
+                    carrierConfig,
+                    evaluatorCallback);
         }
     }
 }
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
index c124a19..2f4cf5e 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
@@ -16,23 +16,32 @@
 
 package com.android.server.vcn.routeselection;
 
+import static com.android.server.VcnManagementService.LOCAL_LOG;
 import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.net.IpSecTransform;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.vcn.VcnManager;
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
+import android.os.Handler;
 import android.os.ParcelUuid;
+import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.VcnContext;
 
+import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 
 /**
  * UnderlyingNetworkEvaluator evaluates the quality and priority class of a network candidate for
@@ -43,20 +52,41 @@
 public class UnderlyingNetworkEvaluator {
     private static final String TAG = UnderlyingNetworkEvaluator.class.getSimpleName();
 
+    private static final int[] PENALTY_TIMEOUT_MINUTES_DEFAULT = new int[] {5};
+
     @NonNull private final VcnContext mVcnContext;
+    @NonNull private final Handler mHandler;
+    @NonNull private final Object mCancellationToken = new Object();
+
     @NonNull private final UnderlyingNetworkRecord.Builder mNetworkRecordBuilder;
 
+    @NonNull private final NetworkEvaluatorCallback mEvaluatorCallback;
+    @NonNull private final List<NetworkMetricMonitor> mMetricMonitors = new ArrayList<>();
+
+    @NonNull private final Dependencies mDependencies;
+
+    // TODO: Support back-off timeouts
+    private long mPenalizedTimeoutMs;
+
     private boolean mIsSelected;
+    private boolean mIsPenalized;
     private int mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID;
 
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
     public UnderlyingNetworkEvaluator(
             @NonNull VcnContext vcnContext,
             @NonNull Network network,
             @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
             @NonNull ParcelUuid subscriptionGroup,
             @NonNull TelephonySubscriptionSnapshot lastSnapshot,
-            @Nullable PersistableBundleWrapper carrierConfig) {
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkEvaluatorCallback evaluatorCallback,
+            @NonNull Dependencies dependencies) {
         mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
+        mHandler = new Handler(mVcnContext.getLooper());
+
+        mDependencies = Objects.requireNonNull(dependencies, "Missing dependencies");
+        mEvaluatorCallback = Objects.requireNonNull(evaluatorCallback, "Missing deps");
 
         Objects.requireNonNull(underlyingNetworkTemplates, "Missing underlyingNetworkTemplates");
         Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
@@ -66,9 +96,76 @@
                 new UnderlyingNetworkRecord.Builder(
                         Objects.requireNonNull(network, "Missing network"));
         mIsSelected = false;
+        mIsPenalized = false;
+        mPenalizedTimeoutMs = getPenaltyTimeoutMs(carrierConfig);
 
         updatePriorityClass(
                 underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+
+        if (isIpSecPacketLossDetectorEnabled()) {
+            try {
+                mMetricMonitors.add(
+                        mDependencies.newIpSecPacketLossDetector(
+                                mVcnContext,
+                                mNetworkRecordBuilder.getNetwork(),
+                                carrierConfig,
+                                new MetricMonitorCallbackImpl()));
+            } catch (IllegalAccessException e) {
+                // No action. Do not add anything to mMetricMonitors
+            }
+        }
+    }
+
+    public UnderlyingNetworkEvaluator(
+            @NonNull VcnContext vcnContext,
+            @NonNull Network network,
+            @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates,
+            @NonNull ParcelUuid subscriptionGroup,
+            @NonNull TelephonySubscriptionSnapshot lastSnapshot,
+            @Nullable PersistableBundleWrapper carrierConfig,
+            @NonNull NetworkEvaluatorCallback evaluatorCallback) {
+        this(
+                vcnContext,
+                network,
+                underlyingNetworkTemplates,
+                subscriptionGroup,
+                lastSnapshot,
+                carrierConfig,
+                evaluatorCallback,
+                new Dependencies());
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static class Dependencies {
+        /** Get an IpSecPacketLossDetector instance */
+        public IpSecPacketLossDetector newIpSecPacketLossDetector(
+                @NonNull VcnContext vcnContext,
+                @NonNull Network network,
+                @Nullable PersistableBundleWrapper carrierConfig,
+                @NonNull NetworkMetricMonitor.NetworkMetricMonitorCallback callback)
+                throws IllegalAccessException {
+            return new IpSecPacketLossDetector(vcnContext, network, carrierConfig, callback);
+        }
+    }
+
+    /** Callback to notify caller to reevaluate network selection */
+    public interface NetworkEvaluatorCallback {
+        /**
+         * Called when mIsPenalized changed
+         *
+         * <p>When receiving this call, UnderlyingNetworkController should reevaluate all network
+         * candidates for VCN underlying network selection
+         */
+        void onEvaluationResultChanged();
+    }
+
+    private class MetricMonitorCallbackImpl
+            implements NetworkMetricMonitor.NetworkMetricMonitorCallback {
+        public void onValidationResultReceived() {
+            mVcnContext.ensureRunningOnLooperThread();
+
+            handleValidationResult();
+        }
     }
 
     private void updatePriorityClass(
@@ -91,8 +188,25 @@
         }
     }
 
-    public static Comparator<UnderlyingNetworkEvaluator> getComparator() {
+    private boolean isIpSecPacketLossDetectorEnabled() {
+        return isIpSecPacketLossDetectorEnabled(mVcnContext);
+    }
+
+    private static boolean isIpSecPacketLossDetectorEnabled(VcnContext vcnContext) {
+        return vcnContext.isFlagIpSecTransformStateEnabled()
+                && vcnContext.isFlagNetworkMetricMonitorEnabled();
+    }
+
+    /** Get the comparator for UnderlyingNetworkEvaluator */
+    public static Comparator<UnderlyingNetworkEvaluator> getComparator(VcnContext vcnContext) {
         return (left, right) -> {
+            if (isIpSecPacketLossDetectorEnabled(vcnContext)) {
+                if (left.mIsPenalized != right.mIsPenalized) {
+                    // A penalized network should have lower priority which means a larger index
+                    return left.mIsPenalized ? 1 : -1;
+                }
+            }
+
             final int leftIndex = left.mPriorityClass;
             final int rightIndex = right.mPriorityClass;
 
@@ -112,6 +226,64 @@
         };
     }
 
+    private static long getPenaltyTimeoutMs(@Nullable PersistableBundleWrapper carrierConfig) {
+        final int[] timeoutMinuteList;
+
+        if (carrierConfig != null) {
+            timeoutMinuteList =
+                    carrierConfig.getIntArray(
+                            VcnManager.VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY,
+                            PENALTY_TIMEOUT_MINUTES_DEFAULT);
+        } else {
+            timeoutMinuteList = PENALTY_TIMEOUT_MINUTES_DEFAULT;
+        }
+
+        // TODO: Add the support of back-off timeouts and return the full list
+        return TimeUnit.MINUTES.toMillis(timeoutMinuteList[0]);
+    }
+
+    private void handleValidationResult() {
+        final boolean wasPenalized = mIsPenalized;
+        mIsPenalized = false;
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            mIsPenalized |= monitor.isValidationFailed();
+        }
+
+        if (wasPenalized == mIsPenalized) {
+            return;
+        }
+
+        logInfo(
+                "#handleValidationResult: wasPenalized "
+                        + wasPenalized
+                        + " mIsPenalized "
+                        + mIsPenalized);
+
+        if (mIsPenalized) {
+            mHandler.postDelayed(
+                    new ExitPenaltyBoxRunnable(), mCancellationToken, mPenalizedTimeoutMs);
+        } else {
+            // Exit the penalty box
+            mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
+        }
+        mEvaluatorCallback.onEvaluationResultChanged();
+    }
+
+    public class ExitPenaltyBoxRunnable implements Runnable {
+        @Override
+        public void run() {
+            if (!mIsPenalized) {
+                logWtf("Evaluator not being penalized but ExitPenaltyBoxRunnable was scheduled");
+                return;
+            }
+
+            // TODO: There might be a future metric monitor (e.g. ping) that will require the
+            // validation to pass before exiting the penalty box.
+            mIsPenalized = false;
+            mEvaluatorCallback.onEvaluationResultChanged();
+        }
+    }
+
     /** Set the NetworkCapabilities */
     public void setNetworkCapabilities(
             @NonNull NetworkCapabilities nc,
@@ -162,6 +334,10 @@
 
         updatePriorityClass(
                 underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            monitor.setIsSelectedUnderlyingNetwork(isSelected);
+        }
     }
 
     /**
@@ -174,6 +350,35 @@
             @Nullable PersistableBundleWrapper carrierConfig) {
         updatePriorityClass(
                 underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+
+        // The already scheduled event will not be affected. The followup events will be scheduled
+        // with the new timeout
+        mPenalizedTimeoutMs = getPenaltyTimeoutMs(carrierConfig);
+
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            monitor.setCarrierConfig(carrierConfig);
+        }
+    }
+
+    /** Update the inbound IpSecTransform applied to the network */
+    public void setInboundTransform(@NonNull IpSecTransform transform) {
+        if (!mIsSelected) {
+            logWtf("setInboundTransform on an unselected evaluator");
+            return;
+        }
+
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            monitor.setInboundTransform(transform);
+        }
+    }
+
+    /** Close the evaluator and stop all the underlying network metric monitors */
+    public void close() {
+        mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
+
+        for (NetworkMetricMonitor monitor : mMetricMonitors) {
+            monitor.close();
+        }
     }
 
     /** Return whether this network evaluator is valid */
@@ -196,6 +401,11 @@
         return mPriorityClass;
     }
 
+    /** Return whether the network is being penalized */
+    public boolean isPenalized() {
+        return mIsPenalized;
+    }
+
     /** Dump the information of this instance */
     public void dump(IndentingPrintWriter pw) {
         pw.println("UnderlyingNetworkEvaluator:");
@@ -211,7 +421,22 @@
 
         pw.println("mIsSelected: " + mIsSelected);
         pw.println("mPriorityClass: " + mPriorityClass);
+        pw.println("mIsPenalized: " + mIsPenalized);
 
         pw.decreaseIndent();
     }
+
+    private String getLogPrefix() {
+        return "[Network " + mNetworkRecordBuilder.getNetwork() + "] ";
+    }
+
+    private void logInfo(String msg) {
+        Slog.i(TAG, getLogPrefix() + msg);
+        LOCAL_LOG.log("[INFO ] " + TAG + getLogPrefix() + msg);
+    }
+
+    private void logWtf(String msg) {
+        Slog.wtf(TAG, getLogPrefix() + msg);
+        LOCAL_LOG.log("[WTF ] " + TAG + getLogPrefix() + msg);
+    }
 }
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index 29782d9..f4fb1a1 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -159,11 +159,28 @@
         }
     }
 
+    private boolean shouldTriggerRepairLocked() {
+        if (mCurrentWebViewPackage == null) {
+            return true;
+        }
+        WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+        if (mCurrentWebViewPackage.packageName.equals(defaultProvider.packageName)) {
+            List<UserPackage> userPackages =
+                    mSystemInterface.getPackageInfoForProviderAllUsers(
+                            mContext, defaultProvider);
+            return !isInstalledAndEnabledForAllUsers(userPackages);
+        } else {
+            return false;
+        }
+    }
+
     @Override
     public void prepareWebViewInSystemServer() {
         try {
+            boolean repairNeeded = true;
             synchronized (mLock) {
                 mCurrentWebViewPackage = findPreferredWebViewPackage();
+                repairNeeded = shouldTriggerRepairLocked();
                 String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext);
                 if (userSetting != null
                         && !userSetting.equals(mCurrentWebViewPackage.packageName)) {
@@ -177,26 +194,25 @@
                 }
                 onWebViewProviderChanged(mCurrentWebViewPackage);
             }
+
+            if (repairNeeded) {
+                // We didn't find a valid WebView implementation. Try explicitly re-enabling the
+                // default package for all users in case it was disabled, even if we already did the
+                // one-time migration before. If this actually changes the state, we will see the
+                // PackageManager broadcast shortly and try again.
+                WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+                Slog.w(
+                        TAG,
+                        "No provider available for all users, trying to enable "
+                                + defaultProvider.packageName);
+                mSystemInterface.enablePackageForAllUsers(
+                        mContext, defaultProvider.packageName, true);
+            }
+
         } catch (Throwable t) {
             // Log and discard errors at this stage as we must not crash the system server.
             Slog.e(TAG, "error preparing webview provider from system server", t);
         }
-
-        if (getCurrentWebViewPackage() == null) {
-            // We didn't find a valid WebView implementation. Try explicitly re-enabling the
-            // fallback package for all users in case it was disabled, even if we already did the
-            // one-time migration before. If this actually changes the state, we will see the
-            // PackageManager broadcast shortly and try again.
-            WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages();
-            WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
-            if (fallbackProvider != null) {
-                Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName);
-                mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName,
-                                                          true);
-            } else {
-                Slog.e(TAG, "No valid provider and no fallback available.");
-            }
-        }
     }
 
     private void startZygoteWhenReady() {
@@ -421,42 +437,43 @@
 
     /**
      * Returns either the package info of the WebView provider determined in the following way:
-     * If the user has chosen a provider then use that if it is valid,
-     * otherwise use the first package in the webview priority list that is valid.
-     *
+     * If the user has chosen a provider then use that if it is valid, enabled and installed
+     * for all users, otherwise use the default provider.
      */
     private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException {
-        ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos();
-
-        String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext);
-
         // If the user has chosen provider, use that (if it's installed and enabled for all
         // users).
-        for (ProviderAndPackageInfo providerAndPackage : providers) {
-            if (providerAndPackage.provider.packageName.equals(userChosenProvider)) {
-                // userPackages can contain null objects.
-                List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
-                                providerAndPackage.provider);
-                if (isInstalledAndEnabledForAllUsers(userPackages)) {
-                    return providerAndPackage.packageInfo;
+        String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext);
+        WebViewProviderInfo userChosenProvider =
+                getWebViewProviderForPackage(userChosenPackageName);
+        if (userChosenProvider != null) {
+            try {
+                PackageInfo packageInfo =
+                        mSystemInterface.getPackageInfoForProvider(userChosenProvider);
+                if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) {
+                    List<UserPackage> userPackages =
+                            mSystemInterface.getPackageInfoForProviderAllUsers(
+                                    mContext, userChosenProvider);
+                    if (isInstalledAndEnabledForAllUsers(userPackages)) {
+                        return packageInfo;
+                    }
                 }
+            } catch (NameNotFoundException e) {
+                Slog.w(TAG, "User chosen WebView package (" + userChosenPackageName
+                        + ") not found");
             }
         }
 
-        // User did not choose, or the choice failed; use the most stable provider that is
-        // installed and enabled for all users, and available by default (not through
-        // user choice).
-        for (ProviderAndPackageInfo providerAndPackage : providers) {
-            if (providerAndPackage.provider.availableByDefault) {
-                // userPackages can contain null objects.
-                List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
-                                providerAndPackage.provider);
-                if (isInstalledAndEnabledForAllUsers(userPackages)) {
-                    return providerAndPackage.packageInfo;
-                }
+        // User did not choose, or the choice failed; return the default provider even if it is not
+        // installed or enabled for all users.
+        WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+        try {
+            PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(defaultProvider);
+            if (validityResult(defaultProvider, packageInfo) == VALIDITY_OK) {
+                return packageInfo;
             }
+        } catch (NameNotFoundException e) {
+            Slog.w(TAG, "Default WebView package (" + defaultProvider.packageName + ") not found");
         }
 
         // This should never happen during normal operation (only with modified system images).
@@ -464,6 +481,16 @@
         throw new WebViewPackageMissingException("Could not find a loadable WebView package");
     }
 
+    private WebViewProviderInfo getWebViewProviderForPackage(String packageName) {
+        WebViewProviderInfo[] allProviders = getWebViewPackages();
+        for (int n = 0; n < allProviders.length; n++) {
+            if (allProviders[n].packageName.equals(packageName)) {
+                return allProviders[n];
+            }
+        }
+        return null;
+    }
+
     /**
      * Return true iff {@param packageInfos} point to only installed and enabled packages.
      * The given packages {@param packageInfos} should all be pointing to the same package, but each
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 8aaf76a..2bd49bf 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1591,7 +1591,6 @@
 
     private static void setLaunchBehind(@NonNull ActivityRecord activity) {
         if (!activity.isVisibleRequested()) {
-            activity.setVisibility(true);
             // The transition could commit the visibility and in the finishing state, that could
             // skip commitVisibility call in setVisibility cause the activity won't visible here.
             // Call it again to make sure the activity could be visible while handling the pending
@@ -1669,10 +1668,14 @@
         ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onBackNavigationDone backType=%s, "
                 + "triggerBack=%b", backType, triggerBack);
 
-        mNavigationMonitor.stopMonitorForRemote();
-        mBackAnimationInProgress = false;
-        mShowWallpaper = false;
-        mPendingAnimationBuilder = null;
+        synchronized (mWindowManagerService.mGlobalLock) {
+            mNavigationMonitor.stopMonitorForRemote();
+            mBackAnimationInProgress = false;
+            mShowWallpaper = false;
+            // All animation should be done, clear any un-send animation.
+            mPendingAnimation = null;
+            mPendingAnimationBuilder = null;
+        }
     }
 
     static TaskSnapshot getSnapshot(@NonNull WindowContainer w,
diff --git a/services/core/java/com/android/server/wm/LegacyTransitionTracer.java b/services/core/java/com/android/server/wm/LegacyTransitionTracer.java
new file mode 100644
index 0000000..fb2d5be
--- /dev/null
+++ b/services/core/java/com/android/server/wm/LegacyTransitionTracer.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.os.Build.IS_USER;
+
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER;
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H;
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L;
+import static com.android.server.wm.shell.TransitionTraceProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.TraceBuffer;
+import com.android.server.wm.Transition.ChangeInfo;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class to collect and dump transition traces.
+ */
+class LegacyTransitionTracer implements TransitionTracer {
+
+    private static final String LOG_TAG = "TransitionTracer";
+
+    private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB
+    private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB
+
+    // This will be the size the proto output streams are initialized to.
+    // Ideally this should fit most or all the proto objects we will create and be no bigger than
+    // that to ensure to don't use excessive amounts of memory.
+    private static final int CHUNK_SIZE = 64;
+
+    static final String WINSCOPE_EXT = ".winscope";
+    private static final String TRACE_FILE =
+            "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT;
+    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+    private final TraceBuffer mTraceBuffer = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY);
+
+    private final Object mEnabledLock = new Object();
+    private volatile boolean mActiveTracingEnabled = false;
+
+    /**
+     * Records key information about a transition that has been sent to Shell to be played.
+     * More information will be appended to the same proto object once the transition is finished or
+     * aborted.
+     * Transition information won't be added to the trace buffer until
+     * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
+     * transition.
+     *
+     * @param transition The transition that has been sent to Shell.
+     * @param targets Information about the target windows of the transition.
+     */
+    @Override
+    public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) {
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+            outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS,
+                    transition.mLogger.mCreateTimeNs);
+            outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS,
+                    transition.mLogger.mSendTimeNs);
+            outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID,
+                    transition.getStartTransaction().getId());
+            outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID,
+                    transition.getFinishTransaction().getId());
+            dumpTransitionTargetsToProto(outputStream, transition, targets);
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            // Don't let any errors in the tracing cause the transition to fail
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
+    /**
+     * Completes the information dumped in {@link #logSentTransition} for a transition
+     * that has finished or aborted, and add the proto object to the trace buffer.
+     *
+     * @param transition The transition that has finished.
+     */
+    @Override
+    public void logFinishedTransition(Transition transition) {
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+            outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS,
+                    transition.mLogger.mFinishTimeNs);
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            // Don't let any errors in the tracing cause the transition to fail
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
+    /**
+     * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
+     * unless actively tracing.
+     *
+     * @param transition The transition that has been aborted
+     */
+    @Override
+    public void logAbortedTransition(Transition transition) {
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+            outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS,
+                    transition.mLogger.mAbortTimeNs);
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            // Don't let any errors in the tracing cause the transition to fail
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
+    @Override
+    public void logRemovingStartingWindow(@NonNull StartingData startingData) {
+        if (startingData.mTransitionId == 0) {
+            return;
+        }
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID,
+                    startingData.mTransitionId);
+            outputStream.write(
+                    com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS,
+                    SystemClock.elapsedRealtimeNanos());
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
+    private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream,
+            Transition transition, ArrayList<ChangeInfo> targets) {
+        Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto");
+        if (mActiveTracingEnabled) {
+            outputStream.write(com.android.server.wm.shell.Transition.ID,
+                    transition.getSyncId());
+        }
+
+        outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType);
+        outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags());
+
+        for (int i = 0; i < targets.size(); ++i) {
+            final long changeToken = outputStream
+                    .start(com.android.server.wm.shell.Transition.TARGETS);
+
+            final Transition.ChangeInfo target = targets.get(i);
+
+            final int layerId;
+            if (target.mContainer.mSurfaceControl.isValid()) {
+                layerId = target.mContainer.mSurfaceControl.getLayerId();
+            } else {
+                layerId = -1;
+            }
+
+            outputStream.write(com.android.server.wm.shell.Target.MODE, target.mReadyMode);
+            outputStream.write(com.android.server.wm.shell.Target.FLAGS, target.mReadyFlags);
+            outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId);
+
+            if (mActiveTracingEnabled) {
+                // What we use in the WM trace
+                final int windowId = System.identityHashCode(target.mContainer);
+                outputStream.write(com.android.server.wm.shell.Target.WINDOW_ID, windowId);
+            }
+
+            outputStream.end(changeToken);
+        }
+
+        Trace.endSection();
+    }
+
+    /**
+     * Starts collecting transitions for the trace.
+     * If called while a trace is already running, this will reset the trace.
+     */
+    @Override
+    public void startTrace(@Nullable PrintWriter pw) {
+        if (IS_USER) {
+            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+            return;
+        }
+        Trace.beginSection("TransitionTracer#startTrace");
+        LogAndPrintln.i(pw, "Starting shell transition trace.");
+        synchronized (mEnabledLock) {
+            mActiveTracingEnabled = true;
+            mTraceBuffer.resetBuffer();
+            mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY);
+        }
+        Trace.endSection();
+    }
+
+    /**
+     * Stops collecting the transition trace and dump to trace to file.
+     *
+     * Dumps the trace to @link{TRACE_FILE}.
+     */
+    @Override
+    public void stopTrace(@Nullable PrintWriter pw) {
+        stopTrace(pw, new File(TRACE_FILE));
+    }
+
+    /**
+     * Stops collecting the transition trace and dump to trace to file.
+     * @param outputFile The file to dump the transition trace to.
+     */
+    public void stopTrace(@Nullable PrintWriter pw, File outputFile) {
+        if (IS_USER) {
+            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+            return;
+        }
+        Trace.beginSection("TransitionTracer#stopTrace");
+        LogAndPrintln.i(pw, "Stopping shell transition trace.");
+        synchronized (mEnabledLock) {
+            mActiveTracingEnabled = false;
+            writeTraceToFileLocked(pw, outputFile);
+            mTraceBuffer.resetBuffer();
+            mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY);
+        }
+        Trace.endSection();
+    }
+
+    /**
+     * Being called while taking a bugreport so that tracing files can be included in the bugreport.
+     *
+     * @param pw Print writer
+     */
+    @Override
+    public void saveForBugreport(@Nullable PrintWriter pw) {
+        if (IS_USER) {
+            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+            return;
+        }
+        Trace.beginSection("TransitionTracer#saveForBugreport");
+        synchronized (mEnabledLock) {
+            final File outputFile = new File(TRACE_FILE);
+            writeTraceToFileLocked(pw, outputFile);
+        }
+        Trace.endSection();
+    }
+
+    @Override
+    public boolean isTracing() {
+        return mActiveTracingEnabled;
+    }
+
+    private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) {
+        Trace.beginSection("TransitionTracer#writeTraceToFileLocked");
+        try {
+            ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE);
+            proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
+            long timeOffsetNs =
+                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
+                            - SystemClock.elapsedRealtimeNanos();
+            proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
+            int pid = android.os.Process.myPid();
+            LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath()
+                    + " from process " + pid);
+            mTraceBuffer.writeTraceToFile(file, proto);
+        } catch (IOException e) {
+            LogAndPrintln.e(pw, "Unable to write buffer to file", e);
+        }
+        Trace.endSection();
+    }
+
+    private static class LogAndPrintln {
+        private static void i(@Nullable PrintWriter pw, String msg) {
+            Log.i(LOG_TAG, msg);
+            if (pw != null) {
+                pw.println(msg);
+                pw.flush();
+            }
+        }
+
+        private static void e(@Nullable PrintWriter pw, String msg) {
+            Log.e(LOG_TAG, msg);
+            if (pw != null) {
+                pw.println("ERROR: " + msg);
+                pw.flush();
+            }
+        }
+
+        private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) {
+            Log.e(LOG_TAG, msg, e);
+            if (pw != null) {
+                pw.println("ERROR: " + msg + " ::\n " + e);
+                pw.flush();
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
new file mode 100644
index 0000000..eae9951
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.internal.perfetto.protos.PerfettoTrace;
+import android.os.SystemClock;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.tracing.transition.TransitionDataSource;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class PerfettoTransitionTracer implements TransitionTracer {
+    private final AtomicInteger mActiveTraces = new AtomicInteger(0);
+    private final TransitionDataSource mDataSource =
+            new TransitionDataSource(this.mActiveTraces::incrementAndGet, () -> {},
+                    this.mActiveTraces::decrementAndGet);
+
+    PerfettoTransitionTracer() {
+        Producer.init(InitArguments.DEFAULTS);
+        mDataSource.register(DataSourceParams.DEFAULTS);
+    }
+
+    /**
+     * Records key information about a transition that has been sent to Shell to be played.
+     * More information will be appended to the same proto object once the transition is finished or
+     * aborted.
+     * Transition information won't be added to the trace buffer until
+     * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
+     * transition.
+     *
+     * @param transition The transition that has been sent to Shell.
+     * @param targets Information about the target windows of the transition.
+     */
+    @Override
+    public void logSentTransition(Transition transition, ArrayList<Transition.ChangeInfo> targets) {
+        if (!isTracing()) {
+            return;
+        }
+
+        mDataSource.trace((ctx) -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+
+            os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+            os.write(PerfettoTrace.ShellTransition.CREATE_TIME_NS,
+                    transition.mLogger.mCreateTimeNs);
+            os.write(PerfettoTrace.ShellTransition.SEND_TIME_NS, transition.mLogger.mSendTimeNs);
+            os.write(PerfettoTrace.ShellTransition.START_TRANSACTION_ID,
+                    transition.getStartTransaction().getId());
+            os.write(PerfettoTrace.ShellTransition.FINISH_TRANSACTION_ID,
+                    transition.getFinishTransaction().getId());
+            os.write(PerfettoTrace.ShellTransition.TYPE, transition.mType);
+            os.write(PerfettoTrace.ShellTransition.FLAGS, transition.getFlags());
+
+            addTransitionTargetsToProto(os, targets);
+
+            os.end(token);
+        });
+    }
+
+    /**
+     * Completes the information dumped in {@link #logSentTransition} for a transition
+     * that has finished or aborted, and add the proto object to the trace buffer.
+     *
+     * @param transition The transition that has finished.
+     */
+    @Override
+    public void logFinishedTransition(Transition transition) {
+        if (!isTracing()) {
+            return;
+        }
+
+        mDataSource.trace((ctx) -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+            os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+            os.write(PerfettoTrace.ShellTransition.FINISH_TIME_NS,
+                    transition.mLogger.mFinishTimeNs);
+            os.end(token);
+        });
+    }
+
+    /**
+     * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
+     * unless actively tracing.
+     *
+     * @param transition The transition that has been aborted
+     */
+    @Override
+    public void logAbortedTransition(Transition transition) {
+        if (!isTracing()) {
+            return;
+        }
+
+        mDataSource.trace((ctx) -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+            os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+            os.write(PerfettoTrace.ShellTransition.WM_ABORT_TIME_NS,
+                    transition.mLogger.mAbortTimeNs);
+            os.end(token);
+        });
+    }
+
+    @Override
+    public void logRemovingStartingWindow(@NonNull StartingData startingData) {
+        if (!isTracing()) {
+            return;
+        }
+
+        mDataSource.trace((ctx) -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+            os.write(PerfettoTrace.ShellTransition.ID, startingData.mTransitionId);
+            os.write(PerfettoTrace.ShellTransition.STARTING_WINDOW_REMOVE_TIME_NS,
+                    SystemClock.elapsedRealtimeNanos());
+            os.end(token);
+        });
+    }
+
+    @Override
+    public void startTrace(PrintWriter pw) {
+        // No-op
+    }
+
+    @Override
+    public void stopTrace(PrintWriter pw) {
+        // No-op
+    }
+
+    @Override
+    public void saveForBugreport(PrintWriter pw) {
+        // Nothing to do here. Handled by Perfetto.
+    }
+
+    @Override
+    public boolean isTracing() {
+        return mActiveTraces.get() > 0;
+    }
+
+    private void addTransitionTargetsToProto(
+            ProtoOutputStream os,
+            ArrayList<Transition.ChangeInfo> targets
+    ) {
+        for (int i = 0; i < targets.size(); ++i) {
+            final Transition.ChangeInfo target = targets.get(i);
+
+            final int layerId;
+            if (target.mContainer.mSurfaceControl.isValid()) {
+                layerId = target.mContainer.mSurfaceControl.getLayerId();
+            } else {
+                layerId = -1;
+            }
+            final int windowId = System.identityHashCode(target.mContainer);
+
+            final long token = os.start(PerfettoTrace.ShellTransition.TARGETS);
+            os.write(PerfettoTrace.ShellTransition.Target.MODE, target.mReadyMode);
+            os.write(PerfettoTrace.ShellTransition.Target.FLAGS, target.mReadyFlags);
+            os.write(PerfettoTrace.ShellTransition.Target.LAYER_ID, layerId);
+            os.write(PerfettoTrace.ShellTransition.Target.WINDOW_ID, windowId);
+            os.end(token);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a7a6bf2..314d720 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -208,6 +208,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
@@ -1702,6 +1703,8 @@
         final ActivityRecord r = findActivityInHistory(newR.mActivityComponent, newR.mUserId);
         if (r == null) return null;
 
+        moveTaskFragmentsToBottomIfNeeded(r, finishCount);
+
         final PooledPredicate f = PooledLambda.obtainPredicate(
                 (ActivityRecord ar, ActivityRecord boundaryActivity) ->
                         finishActivityAbove(ar, boundaryActivity, finishCount),
@@ -1722,6 +1725,50 @@
         return r;
     }
 
+    /**
+     * Moves {@link TaskFragment}s to the bottom if the flag
+     * {@link TaskFragment#isMoveToBottomIfClearWhenLaunch} is {@code true}.
+     */
+    @VisibleForTesting
+    void moveTaskFragmentsToBottomIfNeeded(@NonNull ActivityRecord r, @NonNull int[] finishCount) {
+        final int activityIndex = mChildren.indexOf(r);
+        if (activityIndex < 0) {
+            return;
+        }
+
+        List<TaskFragment> taskFragmentsToMove = null;
+
+        // Find the TaskFragments that need to be moved
+        for (int i = mChildren.size() - 1; i > activityIndex; i--) {
+            final TaskFragment taskFragment = mChildren.get(i).asTaskFragment();
+            if (taskFragment != null && taskFragment.isMoveToBottomIfClearWhenLaunch()) {
+                if (taskFragmentsToMove == null) {
+                    taskFragmentsToMove = new ArrayList<>();
+                }
+                taskFragmentsToMove.add(taskFragment);
+            }
+        }
+        if (taskFragmentsToMove == null) {
+            return;
+        }
+
+        // Move the TaskFragments to the bottom of the Task. Their relative orders are preserved.
+        final int size = taskFragmentsToMove.size();
+        for (int i = 0; i < size; i++) {
+            final TaskFragment taskFragment = taskFragmentsToMove.get(i);
+
+            // The visibility of the TaskFragment may change. Collect it in the transition so that
+            // transition animation can be properly played.
+            mTransitionController.collect(taskFragment);
+
+            positionChildAt(POSITION_BOTTOM, taskFragment, false /* includeParents */);
+        }
+
+        // Treat it as if the TaskFragments are finished so that a transition animation can be
+        // played to send the TaskFragments back and bring the activity to front.
+        finishCount[0] += size;
+    }
+
     private static boolean finishActivityAbove(ActivityRecord r, ActivityRecord boundaryActivity,
             @NonNull int[] finishCount) {
         // Stop operation once we reach the boundary activity.
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index f56759f..7d418ea 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -363,6 +363,12 @@
      */
     private boolean mIsolatedNav;
 
+    /**
+     * Whether the TaskFragment should move to bottom of task when any activity below it is
+     * launched in clear top mode.
+     */
+    private boolean mMoveToBottomIfClearWhenLaunch;
+
     /** When set, will force the task to report as invisible. */
     static final int FLAG_FORCE_HIDDEN_FOR_PINNED_TASK = 1;
     static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1;
@@ -3045,6 +3051,14 @@
         mEmbeddedDimArea = embeddedDimArea;
     }
 
+    void setMoveToBottomIfClearWhenLaunch(boolean moveToBottomIfClearWhenLaunch) {
+        mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch;
+    }
+
+    boolean isMoveToBottomIfClearWhenLaunch() {
+        return mMoveToBottomIfClearWhenLaunch;
+    }
+
     @VisibleForTesting
     boolean isDimmingOnParentTask() {
         return mEmbeddedDimArea == EMBEDDED_DIM_AREA_PARENT_TASK;
diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java
index c59d2d3..0f3fe22 100644
--- a/services/core/java/com/android/server/wm/TransitionTracer.java
+++ b/services/core/java/com/android/server/wm/TransitionTracer.java
@@ -1,323 +1,19 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
 package com.android.server.wm;
 
-import static android.os.Build.IS_USER;
-
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER;
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H;
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L;
-import static com.android.server.wm.shell.TransitionTraceProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.util.Log;
-import android.util.proto.ProtoOutputStream;
 
-import com.android.internal.util.TraceBuffer;
-import com.android.server.wm.Transition.ChangeInfo;
-
-import java.io.File;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
 
-/**
- * Helper class to collect and dump transition traces.
- */
-public class TransitionTracer {
+interface TransitionTracer {
+    void logSentTransition(Transition transition, ArrayList<Transition.ChangeInfo> targets);
+    void logFinishedTransition(Transition transition);
+    void logAbortedTransition(Transition transition);
+    void logRemovingStartingWindow(@NonNull StartingData startingData);
 
-    private static final String LOG_TAG = "TransitionTracer";
-
-    private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB
-    private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB
-
-    // This will be the size the proto output streams are initialized to.
-    // Ideally this should fit most or all the proto objects we will create and be no bigger than
-    // that to ensure to don't use excessive amounts of memory.
-    private static final int CHUNK_SIZE = 64;
-
-    static final String WINSCOPE_EXT = ".winscope";
-    private static final String TRACE_FILE =
-            "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT;
-    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
-
-    private final TraceBuffer mTraceBuffer = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY);
-
-    private final Object mEnabledLock = new Object();
-    private volatile boolean mActiveTracingEnabled = false;
-
-    /**
-     * Records key information about a transition that has been sent to Shell to be played.
-     * More information will be appended to the same proto object once the transition is finished or
-     * aborted.
-     * Transition information won't be added to the trace buffer until
-     * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
-     * transition.
-     *
-     * @param transition The transition that has been sent to Shell.
-     * @param targets Information about the target windows of the transition.
-     */
-    public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) {
-        try {
-            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
-            final long protoToken = outputStream
-                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
-            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
-            outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS,
-                    transition.mLogger.mCreateTimeNs);
-            outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS,
-                    transition.mLogger.mSendTimeNs);
-            outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID,
-                    transition.getStartTransaction().getId());
-            outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID,
-                    transition.getFinishTransaction().getId());
-            dumpTransitionTargetsToProto(outputStream, transition, targets);
-            outputStream.end(protoToken);
-
-            mTraceBuffer.add(outputStream);
-        } catch (Exception e) {
-            // Don't let any errors in the tracing cause the transition to fail
-            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
-        }
-    }
-
-    /**
-     * Completes the information dumped in {@link #logSentTransition} for a transition
-     * that has finished or aborted, and add the proto object to the trace buffer.
-     *
-     * @param transition The transition that has finished.
-     */
-    public void logFinishedTransition(Transition transition) {
-        try {
-            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
-            final long protoToken = outputStream
-                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
-            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
-            outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS,
-                    transition.mLogger.mFinishTimeNs);
-            outputStream.end(protoToken);
-
-            mTraceBuffer.add(outputStream);
-        } catch (Exception e) {
-            // Don't let any errors in the tracing cause the transition to fail
-            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
-        }
-    }
-
-    /**
-     * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
-     * unless actively tracing.
-     *
-     * @param transition The transition that has been aborted
-     */
-    public void logAbortedTransition(Transition transition) {
-        try {
-            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
-            final long protoToken = outputStream
-                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
-            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
-            outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS,
-                    transition.mLogger.mAbortTimeNs);
-            outputStream.end(protoToken);
-
-            mTraceBuffer.add(outputStream);
-        } catch (Exception e) {
-            // Don't let any errors in the tracing cause the transition to fail
-            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
-        }
-    }
-
-    void logRemovingStartingWindow(@NonNull StartingData startingData) {
-        if (startingData.mTransitionId == 0) {
-            return;
-        }
-        try {
-            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
-            final long protoToken = outputStream
-                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
-            outputStream.write(com.android.server.wm.shell.Transition.ID,
-                    startingData.mTransitionId);
-            outputStream.write(
-                    com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS,
-                    SystemClock.elapsedRealtimeNanos());
-            outputStream.end(protoToken);
-
-            mTraceBuffer.add(outputStream);
-        } catch (Exception e) {
-            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
-        }
-    }
-
-    private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream,
-            Transition transition, ArrayList<ChangeInfo> targets) {
-        Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto");
-        if (mActiveTracingEnabled) {
-            outputStream.write(com.android.server.wm.shell.Transition.ID,
-                    transition.getSyncId());
-        }
-
-        outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType);
-        outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags());
-
-        for (int i = 0; i < targets.size(); ++i) {
-            final long changeToken = outputStream
-                    .start(com.android.server.wm.shell.Transition.TARGETS);
-
-            final Transition.ChangeInfo target = targets.get(i);
-
-            final int layerId;
-            if (target.mContainer.mSurfaceControl.isValid()) {
-                layerId = target.mContainer.mSurfaceControl.getLayerId();
-            } else {
-                layerId = -1;
-            }
-
-            outputStream.write(com.android.server.wm.shell.Target.MODE, target.mReadyMode);
-            outputStream.write(com.android.server.wm.shell.Target.FLAGS, target.mReadyFlags);
-            outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId);
-
-            if (mActiveTracingEnabled) {
-                // What we use in the WM trace
-                final int windowId = System.identityHashCode(target.mContainer);
-                outputStream.write(com.android.server.wm.shell.Target.WINDOW_ID, windowId);
-            }
-
-            outputStream.end(changeToken);
-        }
-
-        Trace.endSection();
-    }
-
-    /**
-     * Starts collecting transitions for the trace.
-     * If called while a trace is already running, this will reset the trace.
-     */
-    public void startTrace(@Nullable PrintWriter pw) {
-        if (IS_USER) {
-            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
-            return;
-        }
-        Trace.beginSection("TransitionTracer#startTrace");
-        LogAndPrintln.i(pw, "Starting shell transition trace.");
-        synchronized (mEnabledLock) {
-            mActiveTracingEnabled = true;
-            mTraceBuffer.resetBuffer();
-            mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY);
-        }
-        Trace.endSection();
-    }
-
-    /**
-     * Stops collecting the transition trace and dump to trace to file.
-     *
-     * Dumps the trace to @link{TRACE_FILE}.
-     */
-    public void stopTrace(@Nullable PrintWriter pw) {
-        stopTrace(pw, new File(TRACE_FILE));
-    }
-
-    /**
-     * Stops collecting the transition trace and dump to trace to file.
-     * @param outputFile The file to dump the transition trace to.
-     */
-    public void stopTrace(@Nullable PrintWriter pw, File outputFile) {
-        if (IS_USER) {
-            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
-            return;
-        }
-        Trace.beginSection("TransitionTracer#stopTrace");
-        LogAndPrintln.i(pw, "Stopping shell transition trace.");
-        synchronized (mEnabledLock) {
-            mActiveTracingEnabled = false;
-            writeTraceToFileLocked(pw, outputFile);
-            mTraceBuffer.resetBuffer();
-            mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY);
-        }
-        Trace.endSection();
-    }
-
-    /**
-     * Being called while taking a bugreport so that tracing files can be included in the bugreport.
-     *
-     * @param pw Print writer
-     */
-    public void saveForBugreport(@Nullable PrintWriter pw) {
-        if (IS_USER) {
-            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
-            return;
-        }
-        Trace.beginSection("TransitionTracer#saveForBugreport");
-        synchronized (mEnabledLock) {
-            final File outputFile = new File(TRACE_FILE);
-            writeTraceToFileLocked(pw, outputFile);
-        }
-        Trace.endSection();
-    }
-
-    boolean isActiveTracingEnabled() {
-        return mActiveTracingEnabled;
-    }
-
-    private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) {
-        Trace.beginSection("TransitionTracer#writeTraceToFileLocked");
-        try {
-            ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE);
-            proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
-            long timeOffsetNs =
-                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
-                            - SystemClock.elapsedRealtimeNanos();
-            proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
-            int pid = android.os.Process.myPid();
-            LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath()
-                    + " from process " + pid);
-            mTraceBuffer.writeTraceToFile(file, proto);
-        } catch (IOException e) {
-            LogAndPrintln.e(pw, "Unable to write buffer to file", e);
-        }
-        Trace.endSection();
-    }
-
-    private static class LogAndPrintln {
-        private static void i(@Nullable PrintWriter pw, String msg) {
-            Log.i(LOG_TAG, msg);
-            if (pw != null) {
-                pw.println(msg);
-                pw.flush();
-            }
-        }
-
-        private static void e(@Nullable PrintWriter pw, String msg) {
-            Log.e(LOG_TAG, msg);
-            if (pw != null) {
-                pw.println("ERROR: " + msg);
-                pw.flush();
-            }
-        }
-
-        private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) {
-            Log.e(LOG_TAG, msg, e);
-            if (pw != null) {
-                pw.println("ERROR: " + msg + " ::\n " + e);
-                pw.flush();
-            }
-        }
-    }
+    void startTrace(@Nullable PrintWriter pw);
+    void stopTrace(@Nullable PrintWriter pw);
+    boolean isTracing();
+    void saveForBugreport(@Nullable PrintWriter pw);
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5778ab4..9179acf 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1216,7 +1216,12 @@
 
         mWindowTracing = WindowTracing.createDefaultAndStartLooper(this,
                 Choreographer.getInstance());
-        mTransitionTracer = new TransitionTracer();
+
+        if (android.tracing.Flags.perfettoTransitionTracing()) {
+            mTransitionTracer = new PerfettoTransitionTracer();
+        } else {
+            mTransitionTracer = new LegacyTransitionTracer();
+        }
 
         LocalServices.addService(WindowManagerPolicy.class, mPolicy);
 
@@ -6091,7 +6096,7 @@
 
     @Override
     public boolean isTransitionTraceEnabled() {
-        return mTransitionTracer.isActiveTracingEnabled();
+        return mTransitionTracer.isTracing();
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 59d0210..205ed97 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -36,6 +36,7 @@
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT;
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK;
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH;
 import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS;
 import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
 import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN;
@@ -1034,8 +1035,14 @@
                 launchOpts.remove(WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID);
                 final SafeActivityOptions safeOptions =
                         SafeActivityOptions.fromBundle(launchOpts, caller.mPid, caller.mUid);
+                if (transition != null) {
+                    transition.deferTransitionReady();
+                }
                 waitAsyncStart(() -> mService.mTaskSupervisor.startActivityFromRecents(
                         caller.mPid, caller.mUid, taskId, safeOptions));
+                if (transition != null) {
+                    transition.continueTransitionReady();
+                }
                 break;
             }
             case HIERARCHY_OP_TYPE_REORDER:
@@ -1113,11 +1120,17 @@
                     activityOptions.setCallerDisplayId(DEFAULT_DISPLAY);
                 }
                 final Bundle options = activityOptions != null ? activityOptions.toBundle() : null;
+                if (transition != null) {
+                    transition.deferTransitionReady();
+                }
                 int res = waitAsyncStart(() -> mService.mAmInternal.sendIntentSender(
                         hop.getPendingIntent().getTarget(),
                         hop.getPendingIntent().getWhitelistToken(), 0 /* code */,
                         hop.getActivityIntent(), resolvedType, null /* finishReceiver */,
                         null /* requiredPermission */, options));
+                if (transition != null) {
+                    transition.continueTransitionReady();
+                }
                 if (ActivityManager.isStartResultSuccessful(res)) {
                     effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 }
@@ -1502,6 +1515,11 @@
                         : EMBEDDED_DIM_AREA_TASK_FRAGMENT);
                 break;
             }
+            case OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH: {
+                taskFragment.setMoveToBottomIfClearWhenLaunch(
+                        operation.isMoveToBottomIfClearWhenLaunch());
+                break;
+            }
         }
         return effects;
     }
@@ -1554,6 +1572,17 @@
             return false;
         }
 
+        if ((opType == OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH)
+                && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) {
+            final Throwable exception = new SecurityException(
+                    "Only a system organizer can perform "
+                            + "OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH."
+            );
+            sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment,
+                    opType, exception);
+            return false;
+        }
+
         final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken();
         return secondaryFragmentToken == null
                 || validateTaskFragment(mLaunchTaskFragments.get(secondaryFragmentToken), opType,
diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
index 4cd018b..50d48b7 100644
--- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp
+++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
@@ -44,6 +44,7 @@
     MOUSE,
     TOUCHSCREEN,
     DPAD,
+    STYLUS,
 };
 
 static unique_fd invalidFd() {
@@ -98,6 +99,24 @@
             ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOUCH_MAJOR);
             ioctl(fd, UI_SET_ABSBIT, ABS_MT_PRESSURE);
             ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
+            break;
+        case DeviceType::STYLUS:
+            ioctl(fd, UI_SET_EVBIT, EV_ABS);
+            ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH);
+            ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS);
+            ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS2);
+            ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_PEN);
+            ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_RUBBER);
+            ioctl(fd, UI_SET_ABSBIT, ABS_X);
+            ioctl(fd, UI_SET_ABSBIT, ABS_Y);
+            ioctl(fd, UI_SET_ABSBIT, ABS_TILT_X);
+            ioctl(fd, UI_SET_ABSBIT, ABS_TILT_Y);
+            ioctl(fd, UI_SET_ABSBIT, ABS_PRESSURE);
+            ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
+            break;
+        default:
+            ALOGE("Invalid input device type %d", static_cast<int32_t>(deviceType));
+            return invalidFd();
     }
 
     int version;
@@ -158,6 +177,47 @@
                 ALOGE("Error creating touchscreen uinput tracking ids: %s", strerror(errno));
                 return invalidFd();
             }
+        } else if (deviceType == DeviceType::STYLUS) {
+            uinput_abs_setup xAbsSetup;
+            xAbsSetup.code = ABS_X;
+            xAbsSetup.absinfo.maximum = screenWidth - 1;
+            xAbsSetup.absinfo.minimum = 0;
+            if (ioctl(fd, UI_ABS_SETUP, &xAbsSetup) != 0) {
+                ALOGE("Error creating stylus uinput x axis: %s", strerror(errno));
+                return invalidFd();
+            }
+            uinput_abs_setup yAbsSetup;
+            yAbsSetup.code = ABS_Y;
+            yAbsSetup.absinfo.maximum = screenHeight - 1;
+            yAbsSetup.absinfo.minimum = 0;
+            if (ioctl(fd, UI_ABS_SETUP, &yAbsSetup) != 0) {
+                ALOGE("Error creating stylus uinput y axis: %s", strerror(errno));
+                return invalidFd();
+            }
+            uinput_abs_setup tiltXAbsSetup;
+            tiltXAbsSetup.code = ABS_TILT_X;
+            tiltXAbsSetup.absinfo.maximum = 90;
+            tiltXAbsSetup.absinfo.minimum = -90;
+            if (ioctl(fd, UI_ABS_SETUP, &tiltXAbsSetup) != 0) {
+                ALOGE("Error creating stylus uinput tilt x axis: %s", strerror(errno));
+                return invalidFd();
+            }
+            uinput_abs_setup tiltYAbsSetup;
+            tiltYAbsSetup.code = ABS_TILT_Y;
+            tiltYAbsSetup.absinfo.maximum = 90;
+            tiltYAbsSetup.absinfo.minimum = -90;
+            if (ioctl(fd, UI_ABS_SETUP, &tiltYAbsSetup) != 0) {
+                ALOGE("Error creating stylus uinput tilt y axis: %s", strerror(errno));
+                return invalidFd();
+            }
+            uinput_abs_setup pressureAbsSetup;
+            pressureAbsSetup.code = ABS_PRESSURE;
+            pressureAbsSetup.absinfo.maximum = 255;
+            pressureAbsSetup.absinfo.minimum = 0;
+            if (ioctl(fd, UI_ABS_SETUP, &pressureAbsSetup) != 0) {
+                ALOGE("Error creating touchscreen uinput pressure axis: %s", strerror(errno));
+                return invalidFd();
+            }
         }
         if (ioctl(fd, UI_DEV_SETUP, &setup) != 0) {
             ALOGE("Error creating uinput device: %s", strerror(errno));
@@ -182,6 +242,17 @@
             fallback.absmax[ABS_MT_TOUCH_MAJOR] = screenWidth - 1;
             fallback.absmin[ABS_MT_PRESSURE] = 0;
             fallback.absmax[ABS_MT_PRESSURE] = 255;
+        } else if (deviceType == DeviceType::STYLUS) {
+            fallback.absmin[ABS_X] = 0;
+            fallback.absmax[ABS_X] = screenWidth - 1;
+            fallback.absmin[ABS_Y] = 0;
+            fallback.absmax[ABS_Y] = screenHeight - 1;
+            fallback.absmin[ABS_TILT_X] = -90;
+            fallback.absmax[ABS_TILT_X] = 90;
+            fallback.absmin[ABS_TILT_Y] = -90;
+            fallback.absmax[ABS_TILT_Y] = 90;
+            fallback.absmin[ABS_PRESSURE] = 0;
+            fallback.absmax[ABS_PRESSURE] = 255;
         }
         if (TEMP_FAILURE_RETRY(write(fd, &fallback, sizeof(fallback))) != sizeof(fallback)) {
             ALOGE("Error creating uinput device: %s", strerror(errno));
@@ -234,6 +305,13 @@
     return fd.ok() ? reinterpret_cast<jlong>(new VirtualTouchscreen(std::move(fd))) : INVALID_PTR;
 }
 
+static jlong nativeOpenUinputStylus(JNIEnv* env, jobject thiz, jstring name, jint vendorId,
+                                    jint productId, jstring phys, jint height, jint width) {
+    auto fd =
+            openUinputJni(env, name, vendorId, productId, phys, DeviceType::STYLUS, height, width);
+    return fd.ok() ? reinterpret_cast<jlong>(new VirtualStylus(std::move(fd))) : INVALID_PTR;
+}
+
 static void nativeCloseUinput(JNIEnv* env, jobject thiz, jlong ptr) {
     VirtualInputDevice* virtualInputDevice = reinterpret_cast<VirtualInputDevice*>(ptr);
     delete virtualInputDevice;
@@ -287,6 +365,22 @@
                                           std::chrono::nanoseconds(eventTimeNanos));
 }
 
+// Native methods for VirtualStylus
+static bool nativeWriteStylusMotionEvent(JNIEnv* env, jobject thiz, jlong ptr, jint toolType,
+                                         jint action, jint locationX, jint locationY, jint pressure,
+                                         jint tiltX, jint tiltY, jlong eventTimeNanos) {
+    VirtualStylus* virtualStylus = reinterpret_cast<VirtualStylus*>(ptr);
+    return virtualStylus->writeMotionEvent(toolType, action, locationX, locationY, pressure, tiltX,
+                                           tiltY, std::chrono::nanoseconds(eventTimeNanos));
+}
+
+static bool nativeWriteStylusButtonEvent(JNIEnv* env, jobject thiz, jlong ptr, jint buttonCode,
+                                         jint action, jlong eventTimeNanos) {
+    VirtualStylus* virtualStylus = reinterpret_cast<VirtualStylus*>(ptr);
+    return virtualStylus->writeButtonEvent(buttonCode, action,
+                                           std::chrono::nanoseconds(eventTimeNanos));
+}
+
 static JNINativeMethod methods[] = {
         {"nativeOpenUinputDpad", "(Ljava/lang/String;IILjava/lang/String;)J",
          (void*)nativeOpenUinputDpad},
@@ -296,6 +390,8 @@
          (void*)nativeOpenUinputMouse},
         {"nativeOpenUinputTouchscreen", "(Ljava/lang/String;IILjava/lang/String;II)J",
          (void*)nativeOpenUinputTouchscreen},
+        {"nativeOpenUinputStylus", "(Ljava/lang/String;IILjava/lang/String;II)J",
+         (void*)nativeOpenUinputStylus},
         {"nativeCloseUinput", "(J)V", (void*)nativeCloseUinput},
         {"nativeWriteDpadKeyEvent", "(JIIJ)Z", (void*)nativeWriteDpadKeyEvent},
         {"nativeWriteKeyEvent", "(JIIJ)Z", (void*)nativeWriteKeyEvent},
@@ -303,6 +399,8 @@
         {"nativeWriteTouchEvent", "(JIIIFFFFJ)Z", (void*)nativeWriteTouchEvent},
         {"nativeWriteRelativeEvent", "(JFFJ)Z", (void*)nativeWriteRelativeEvent},
         {"nativeWriteScrollEvent", "(JFFJ)Z", (void*)nativeWriteScrollEvent},
+        {"nativeWriteStylusMotionEvent", "(JIIIIIIIJ)Z", (void*)nativeWriteStylusMotionEvent},
+        {"nativeWriteStylusButtonEvent", "(JIIJ)Z", (void*)nativeWriteStylusButtonEvent},
 };
 
 int register_android_server_companion_virtual_InputController(JNIEnv* env) {
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 3cbceec..a469165 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -625,6 +625,9 @@
 
     If no mode is specified, the mapping will be used for the default mode.
     If no setting is specified, the mapping will be used for the normal brightness setting.
+
+    If no mapping is defined for one of the settings, the mapping for the normal setting will be
+    used as a fallback.
     -->
     <xs:complexType name="luxToBrightnessMapping">
         <xs:element name="map" type="nonNegativeFloatToFloatMap">
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index fb0729f..667e086 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -63,7 +63,6 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.view.autofill.IAutoFillManagerClient;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.credentials.metrics.ApiName;
@@ -484,7 +483,7 @@
         public ICancellationSignal getCandidateCredentials(
                 GetCredentialRequest request,
                 IGetCandidateCredentialsCallback callback,
-                IAutoFillManagerClient clientCallback,
+                IBinder clientBinder,
                 final String callingPackage) {
             Slog.i(TAG, "starting getCandidateCredentials with callingPackage: "
                     + callingPackage);
@@ -506,7 +505,7 @@
                             constructCallingAppInfo(callingPackage, userId, request.getOrigin()),
                             getEnabledProvidersForUser(userId),
                             CancellationSignal.fromTransport(cancelTransport),
-                            clientCallback
+                            clientBinder
                     );
             addSessionLocked(userId, session);
 
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index 0187ce8..25281ba 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -30,11 +30,11 @@
 import android.credentials.ui.ProviderData;
 import android.credentials.ui.RequestInfo;
 import android.os.CancellationSignal;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.service.credentials.CallingAppInfo;
 import android.service.credentials.PermissionUtils;
 import android.util.Slog;
-import android.view.autofill.IAutoFillManagerClient;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -52,7 +52,7 @@
     private static final String SESSION_ID_KEY = "autofill_session_id";
     private static final String REQUEST_ID_KEY = "autofill_request_id";
 
-    private final IAutoFillManagerClient mAutoFillCallback;
+    private final IBinder mClientBinder;
     private final int mAutofillSessionId;
     private final int mAutofillRequestId;
 
@@ -62,15 +62,15 @@
             IGetCandidateCredentialsCallback callback, GetCredentialRequest request,
             CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders,
             CancellationSignal cancellationSignal,
-            IAutoFillManagerClient autoFillCallback) {
+            IBinder clientBinder) {
         super(context, sessionCallback, lock, userId, callingUid, request, callback,
                 RequestInfo.TYPE_GET, callingAppInfo, enabledProviders,
                 cancellationSignal, 0L, /*shouldBindClientToDeath=*/ false);
-        mAutoFillCallback = autoFillCallback;
+        mClientBinder = clientBinder;
         mAutofillSessionId = request.getData().getInt(SESSION_ID_KEY, -1);
         mAutofillRequestId = request.getData().getInt(REQUEST_ID_KEY, -1);
-        if (mAutoFillCallback != null) {
-            setUpClientCallbackListener(mAutoFillCallback.asBinder());
+        if (mClientBinder != null) {
+            setUpClientCallbackListener(mClientBinder);
         }
     }
 
diff --git a/services/foldables/devicestateprovider/proguard.flags b/services/foldables/devicestateprovider/proguard.flags
index 069cbc6..b810cad 100644
--- a/services/foldables/devicestateprovider/proguard.flags
+++ b/services/foldables/devicestateprovider/proguard.flags
@@ -1 +1 @@
--keep,allowoptimization,allowaccessmodification class com.android.server.policy.TentModeDeviceStatePolicy { *; }
+-keep,allowoptimization,allowaccessmodification class com.android.server.policy.BookStyleDeviceStatePolicy { *; }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
new file mode 100644
index 0000000..d5a3cff
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen.OUTER;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0_TO_45;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_45_TO_90;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_90_TO_180;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.util.ArraySet;
+import android.view.Display;
+import android.view.Surface;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition;
+import com.android.server.policy.BookStyleClosedStatePredicate.ConditionSensorListener.SensorSubscription;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * 'Closed' state predicate that takes into account the posture of the device
+ * It accepts list of state transitions that control how the device moves between
+ * device states.
+ * See {@link BookStyleStateTransitions} for detailed description of the default behavior.
+ */
+public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceStateProvider>,
+        DisplayManager.DisplayListener {
+
+    private final BookStylePreferredScreenCalculator mClosedStateCalculator;
+    private final Handler mHandler = new Handler();
+    private final PostureEstimator mPostureEstimator;
+    private final DisplayManager mDisplayManager;
+
+    /**
+     * Creates {@link BookStyleClosedStatePredicate}. It is expected that the device has a pair
+     * of accelerometer sensors (one for each movable part of the device), see parameter
+     * descriptions for the behaviour when these sensors are not available.
+     * @param context context that could be used to get system services
+     * @param updatesListener callback that will be executed whenever the predicate should be
+     *                        checked again
+     * @param leftAccelerometerSensor accelerometer sensor that is located in the half of the
+     *                                device that has the outer screen, in case if this sensor is
+     *                                not provided, tent/wedge mode will be detected only using
+     *                                orientation sensor and screen rotation, so this mode won't
+     *                                be accessible by putting the device on a flat surface
+     * @param rightAccelerometerSensor accelerometer sensor that is located on the opposite side
+     *                                 across the hinge from the previous accelerometer sensor,
+     *                                 in case if this sensor is not provided, reverse wedge mode
+     *                                 won't be detected, so the device will use closed state using
+     *                                 constant angle when folding
+     * @param stateTransitions definition of all possible state transitions, see
+     *                         {@link BookStyleStateTransitions} for sample and more details
+     */
+
+    public BookStyleClosedStatePredicate(@NonNull Context context,
+            @NonNull ClosedStateUpdatesListener updatesListener,
+            @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+            @NonNull List<StateTransition> stateTransitions) {
+        mDisplayManager = context.getSystemService(DisplayManager.class);
+        mDisplayManager.registerDisplayListener(this, mHandler);
+
+        mClosedStateCalculator = new BookStylePreferredScreenCalculator(stateTransitions);
+
+        final SensorManager sensorManager = context.getSystemService(SensorManager.class);
+        final Sensor orientationSensor = sensorManager.getDefaultSensor(
+                Sensor.TYPE_DEVICE_ORIENTATION);
+
+        mPostureEstimator = new PostureEstimator(mHandler, sensorManager,
+                leftAccelerometerSensor, rightAccelerometerSensor, orientationSensor,
+                updatesListener::onClosedStateUpdated);
+    }
+
+    /**
+     * Based on the current sensor readings and current state, returns true if the device should use
+     * 'CLOSED' device state and false if it should not use 'CLOSED' state (e.g. could use half-open
+     * or open states).
+     */
+    @Override
+    public boolean test(FoldableDeviceStateProvider foldableDeviceStateProvider) {
+        final HingeAngle hingeAngle = hingeAngleFromFloat(
+                foldableDeviceStateProvider.getHingeAngle());
+
+        mPostureEstimator.onDeviceClosedStatusChanged(hingeAngle == ANGLE_0);
+
+        final PreferredScreen preferredScreen = mClosedStateCalculator.
+                calculatePreferredScreen(hingeAngle, mPostureEstimator.isLikelyTentOrWedgeMode(),
+                        mPostureEstimator.isLikelyReverseWedgeMode(hingeAngle));
+
+        return preferredScreen == OUTER;
+    }
+
+    private HingeAngle hingeAngleFromFloat(float hingeAngle) {
+        if (hingeAngle == 0f) {
+            return ANGLE_0;
+        } else if (hingeAngle < 45f) {
+            return ANGLE_0_TO_45;
+        } else if (hingeAngle < 90f) {
+            return ANGLE_45_TO_90;
+        } else {
+            return ANGLE_90_TO_180;
+        }
+    }
+
+    @Override
+    public void onDisplayChanged(int displayId) {
+        if (displayId == DEFAULT_DISPLAY) {
+            final Display display = mDisplayManager.getDisplay(displayId);
+            int displayState = display.getState();
+            boolean isDisplayOn = displayState == Display.STATE_ON;
+            mPostureEstimator.onDisplayPowerStatusChanged(isDisplayOn);
+            mPostureEstimator.onDisplayRotationChanged(display.getRotation());
+        }
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) {
+
+    }
+
+    @Override
+    public void onDisplayRemoved(int displayId) {
+
+    }
+
+    public interface ClosedStateUpdatesListener {
+        void onClosedStateUpdated();
+    }
+
+    /**
+     * Estimates if the device is going to enter wedge/tent mode based on the sensor data
+     */
+    private static class PostureEstimator implements SensorEventListener {
+
+
+        private static final int FLAT_INCLINATION_THRESHOLD_DEGREES = 8;
+
+        /**
+         * Alpha parameter of the accelerometer low pass filter: the lower the value, the less high
+         * frequency noise it filter but reduces the latency.
+         */
+        private static final float GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE = 0.8f;
+
+
+        @Nullable
+        private final Sensor mLeftAccelerometerSensor;
+        @Nullable
+        private final Sensor mRightAccelerometerSensor;
+        private final Sensor mOrientationSensor;
+        private final Runnable mOnSensorUpdatedListener;
+
+        private final ConditionSensorListener mConditionedSensorListener;
+
+        @Nullable
+        private float[] mRightGravityVector;
+
+        @Nullable
+        private float[] mLeftGravityVector;
+
+        @Nullable
+        private Integer mLastScreenRotation;
+
+        @Nullable
+        private SensorEvent mLastDeviceOrientationSensorEvent = null;
+
+        private boolean mScreenTurnedOn = false;
+        private boolean mDeviceClosed = false;
+
+        public PostureEstimator(Handler handler, SensorManager sensorManager,
+                @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+                Sensor orientationSensor, Runnable onSensorUpdated) {
+            mLeftAccelerometerSensor = leftAccelerometerSensor;
+            mRightAccelerometerSensor = rightAccelerometerSensor;
+            mOrientationSensor = orientationSensor;
+
+            mOnSensorUpdatedListener = onSensorUpdated;
+
+            final List<SensorSubscription> sensorSubscriptions = new ArrayList<>();
+            if (mLeftAccelerometerSensor != null) {
+                sensorSubscriptions.add(new SensorSubscription(
+                        mLeftAccelerometerSensor,
+                        /* allowedToListen= */ () -> mScreenTurnedOn && !mDeviceClosed,
+                        /* cleanup= */ () -> mLeftGravityVector = null));
+            }
+
+            if (mRightAccelerometerSensor != null) {
+                sensorSubscriptions.add(new SensorSubscription(
+                        mRightAccelerometerSensor,
+                        /* allowedToListen= */ () -> mScreenTurnedOn,
+                        /* cleanup= */ () -> mRightGravityVector = null));
+            }
+
+            sensorSubscriptions.add(new SensorSubscription(mOrientationSensor,
+                    /* allowedToListen= */ () -> mScreenTurnedOn,
+                    /* cleanup= */ () -> mLastDeviceOrientationSensorEvent = null));
+
+            mConditionedSensorListener = new ConditionSensorListener(sensorManager, this, handler,
+                    sensorSubscriptions);
+        }
+
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            if (event.sensor == mRightAccelerometerSensor) {
+                if (mRightGravityVector == null) {
+                    mRightGravityVector = new float[3];
+                }
+                setNewValueWithHighPassFilter(mRightGravityVector, event.values);
+
+                final boolean isRightMostlyFlat = Objects.equals(
+                        isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE);
+
+                if (isRightMostlyFlat) {
+                    // Reset orientation sensor when the device becomes flat
+                    mLastDeviceOrientationSensorEvent = null;
+                }
+            } else if (event.sensor == mLeftAccelerometerSensor) {
+                if (mLeftGravityVector == null) {
+                    mLeftGravityVector = new float[3];
+                }
+                setNewValueWithHighPassFilter(mLeftGravityVector, event.values);
+            } else if (event.sensor == mOrientationSensor) {
+                mLastDeviceOrientationSensorEvent = event;
+            }
+
+            mOnSensorUpdatedListener.run();
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+
+        }
+
+        private void setNewValueWithHighPassFilter(float[] output, float[] newValues) {
+            final float alpha = GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE;
+            output[0] = alpha * output[0] + (1 - alpha) * newValues[0];
+            output[1] = alpha * output[1] + (1 - alpha) * newValues[1];
+            output[2] = alpha * output[2] + (1 - alpha) * newValues[2];
+        }
+
+        /**
+         * Returns true if the phone likely in reverse wedge mode (when a foldable phone is lying
+         * on the outer screen mostly flat to the ground)
+         */
+        public boolean isLikelyReverseWedgeMode(HingeAngle hingeAngle) {
+            return hingeAngle != ANGLE_0 && Objects.equals(
+                    isGravityVectorMostlyFlat(mLeftGravityVector), Boolean.TRUE);
+        }
+
+        /**
+         * Returns true if the phone is likely in tent or wedge mode when unfolding. Tent mode
+         * is detected by checking if the phone is in seascape position, screen is rotated to
+         * landscape or seascape, or if the right side of the device is mostly flat.
+         */
+        public boolean isLikelyTentOrWedgeMode() {
+            boolean isScreenLandscapeOrSeascape = Objects.equals(mLastScreenRotation,
+                    Surface.ROTATION_270) || Objects.equals(mLastScreenRotation,
+                    Surface.ROTATION_90);
+            if (isScreenLandscapeOrSeascape) {
+                return true;
+            }
+
+            boolean isRightMostlyFlat = Objects.equals(
+                    isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE);
+            if (isRightMostlyFlat) {
+                return true;
+            }
+
+            boolean isSensorSeaScape = Objects.equals(getOrientationSensorRotation(),
+                    Surface.ROTATION_270);
+            if (isSensorSeaScape) {
+                return true;
+            }
+
+            return false;
+        }
+
+        /**
+         * Returns true if the passed gravity vector implies that the phone is mostly flat (the
+         * vector is close to be perpendicular to the ground and has a positive Z component).
+         * Returns null if there is no data from the sensor.
+         */
+        private Boolean isGravityVectorMostlyFlat(@Nullable float[] vector) {
+            if (vector == null) return null;
+            if (vector[0] == 0.0f && vector[1] == 0.0f && vector[2] == 0.0f) {
+                // Likely we haven't received the actual data yet, treat it as no data
+                return null;
+            }
+
+            double vectorMagnitude = Math.sqrt(
+                    vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
+            float normalizedGravityZ = (float) (vector[2] / vectorMagnitude);
+
+            final int inclination = (int) Math.round(Math.toDegrees(Math.acos(normalizedGravityZ)));
+            return inclination < FLAT_INCLINATION_THRESHOLD_DEGREES;
+        }
+
+        private Integer getOrientationSensorRotation() {
+            if (mLastDeviceOrientationSensorEvent == null) return null;
+            return (int) mLastDeviceOrientationSensorEvent.values[0];
+        }
+
+        /**
+         * Called whenever display status changes, we use this signal to start/stop listening
+         * to sensors when the display is off to save battery. Using display state instead of
+         * general power state to reduce the time when sensors are on, we don't need to listen
+         * to the extra sensors when the screen is off.
+         */
+        public void onDisplayPowerStatusChanged(boolean screenTurnedOn) {
+            mScreenTurnedOn = screenTurnedOn;
+            mConditionedSensorListener.updateListeningState();
+        }
+
+        /**
+         * Called whenever we display rotation might have been updated
+         * @param rotation new rotation
+         */
+        public void onDisplayRotationChanged(int rotation) {
+            mLastScreenRotation = rotation;
+        }
+
+        /**
+         * Called whenever foldable device becomes fully closed or opened
+         */
+        public void onDeviceClosedStatusChanged(boolean deviceClosed) {
+            mDeviceClosed = deviceClosed;
+            mConditionedSensorListener.updateListeningState();
+        }
+    }
+
+    /**
+     * Helper class that subscribes or unsubscribes from a sensor based on a condition specified
+     * in {@link SensorSubscription}
+     */
+    static class ConditionSensorListener {
+        private final List<SensorSubscription> mSensorSubscriptions;
+        private final ArraySet<Sensor> mIsListening = new ArraySet<>();
+
+        private final SensorManager mSensorManager;
+        private final SensorEventListener mSensorEventListener;
+
+        private final Handler mHandler;
+
+        public ConditionSensorListener(SensorManager sensorManager,
+                SensorEventListener sensorEventListener, Handler handler,
+                List<SensorSubscription> sensorSubscriptions) {
+            mSensorManager = sensorManager;
+            mSensorEventListener = sensorEventListener;
+            mSensorSubscriptions = sensorSubscriptions;
+            mHandler = handler;
+        }
+
+        /**
+         * Updates current listening state of the sensor based on the provided conditions
+         */
+        public void updateListeningState() {
+            for (int i = 0; i < mSensorSubscriptions.size(); i++) {
+                final SensorSubscription subscription = mSensorSubscriptions.get(i);
+                final Sensor sensor = subscription.mSensor;
+
+                final boolean shouldBeListening = subscription.mAllowedToListenSupplier.get();
+                final boolean isListening = mIsListening.contains(sensor);
+                final boolean shouldUpdateListening = isListening != shouldBeListening;
+
+                if (shouldUpdateListening) {
+                    if (shouldBeListening) {
+                        mIsListening.add(sensor);
+                        mSensorManager.registerListener(mSensorEventListener, sensor,
+                                SENSOR_DELAY_NORMAL, mHandler);
+                    } else {
+                        mIsListening.remove(sensor);
+                        mSensorManager.unregisterListener(mSensorEventListener, sensor);
+                        subscription.mOnUnsubscribe.run();
+                    }
+                }
+            }
+        }
+
+        /**
+         * Represents a configuration of a single sensor subscription
+         */
+        public static class SensorSubscription {
+            private final Sensor mSensor;
+            private final Supplier<Boolean> mAllowedToListenSupplier;
+            private final Runnable mOnUnsubscribe;
+
+            /**
+             * @param sensor sensor to listen to
+             * @param allowedToListen return true when it is allowed to listen to the sensor
+             * @param cleanup a runnable that will be closed just before unsubscribing from the
+             *                sensor
+             */
+
+            public SensorSubscription(Sensor sensor, Supplier<Boolean> allowedToListen,
+                    Runnable cleanup) {
+                mSensor = sensor;
+                mAllowedToListenSupplier = allowedToListen;
+                mOnUnsubscribe = cleanup;
+            }
+        }
+    }
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
similarity index 73%
rename from services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
rename to services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index 5968b63..ad938af 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
@@ -21,10 +21,12 @@
 import static com.android.server.devicestate.DeviceState.FLAG_EMULATED_ONLY;
 import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE;
 import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL;
+import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS;
 import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
 import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createTentModeClosedState;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
@@ -39,12 +41,15 @@
 import java.util.function.Predicate;
 
 /**
- * Device state policy for a foldable device that supports tent mode: a mode when the device
- * keeps the outer display on until reaching a certain hinge angle threshold.
+ * Device state policy for a foldable device with two screens in a book style, where the hinge is
+ * located on the left side of the device when in folded posture.
+ * The policy supports tent/wedge mode: a mode when the device keeps the outer display on
+ * until reaching certain conditions like hinge angle threshold.
  *
  * Contains configuration for {@link FoldableDeviceStateProvider}.
  */
-public class TentModeDeviceStatePolicy extends DeviceStatePolicy {
+public class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements
+        BookStyleClosedStatePredicate.ClosedStateUpdatesListener {
 
     private static final int DEVICE_STATE_CLOSED = 0;
     private static final int DEVICE_STATE_HALF_OPENED = 1;
@@ -57,9 +62,10 @@
     private static final int MIN_CLOSED_ANGLE_DEGREES = 0;
     private static final int MAX_CLOSED_ANGLE_DEGREES = 5;
 
-    private final DeviceStateProvider mProvider;
+    private final FoldableDeviceStateProvider mProvider;
 
     private final boolean mIsDualDisplayBlockingEnabled;
+    private final boolean mEnablePostureBasedClosedState;
     private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
     private static final Predicate<FoldableDeviceStateProvider> NOT_ALLOWED = p -> false;
 
@@ -73,30 +79,30 @@
      *                          between folded and unfolded modes, otherwise when folding the
      *                          display switch will happen at 0 degrees
      */
-    public TentModeDeviceStatePolicy(@NonNull Context context,
-            @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, int closeAngleDegrees) {
-        this(new FeatureFlagsImpl(), context, hingeAngleSensor, hallSensor, closeAngleDegrees);
-    }
-
-    public TentModeDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
-                                     @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
-                                     int closeAngleDegrees) {
+    public BookStyleDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
+            @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
+            @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+            Integer closeAngleDegrees) {
         super(context);
 
         final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
         final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
 
-        final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees);
-
+        mEnablePostureBasedClosedState = featureFlags.enableFoldablesPostureBasedClosedState();
         mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
 
+        final DeviceStateConfiguration[] configuration = createConfiguration(
+                leftAccelerometerSensor, rightAccelerometerSensor, closeAngleDegrees);
+
         mProvider = new FoldableDeviceStateProvider(mContext, sensorManager,
                 hingeAngleSensor, hallSensor, displayManager, configuration);
     }
 
-    private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) {
+    private DeviceStateConfiguration[] createConfiguration(@Nullable Sensor leftAccelerometerSensor,
+            @Nullable Sensor rightAccelerometerSensor, Integer closeAngleDegrees) {
         return new DeviceStateConfiguration[]{
-                createClosedConfiguration(closeAngleDegrees),
+                createClosedConfiguration(leftAccelerometerSensor, rightAccelerometerSensor,
+                        closeAngleDegrees),
                 createConfig(DEVICE_STATE_HALF_OPENED,
                         /* name= */ "HALF_OPENED",
                         /* activeStatePredicate= */ (provider) -> {
@@ -123,8 +129,10 @@
         };
     }
 
-    private DeviceStateConfiguration createClosedConfiguration(int closeAngleDegrees) {
-        if (closeAngleDegrees > 0) {
+    private DeviceStateConfiguration createClosedConfiguration(
+            @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+            @Nullable Integer closeAngleDegrees) {
+        if (closeAngleDegrees != null) {
             // Switch displays at closeAngleDegrees in both ways (folding and unfolding)
             return createConfig(
                     DEVICE_STATE_CLOSED,
@@ -137,6 +145,19 @@
             );
         }
 
+        if (mEnablePostureBasedClosedState) {
+            // Use smart closed state predicate that will use different switch angles
+            // based on the device posture (e.g. wedge mode, tent mode, reverse wedge mode)
+            return createConfig(
+                    DEVICE_STATE_CLOSED,
+                    /* name= */ "CLOSED",
+                    /* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS,
+                    /* activeStatePredicate= */ new BookStyleClosedStatePredicate(mContext,
+                            this, leftAccelerometerSensor, rightAccelerometerSensor,
+                            DEFAULT_STATE_TRANSITIONS)
+            );
+        }
+
         // Switch to the outer display only at 0 degrees but use TENT_MODE_SWITCH_ANGLE_DEGREES
         // angle when switching to the inner display
         return createTentModeClosedState(DEVICE_STATE_CLOSED,
@@ -148,6 +169,11 @@
     }
 
     @Override
+    public void onClosedStateUpdated() {
+        mProvider.notifyDeviceStateChangedIfNeeded();
+    }
+
+    @Override
     public DeviceStateProvider getDeviceStateProvider() {
         return mProvider;
     }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
new file mode 100644
index 0000000..8977422
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import android.annotation.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Calculates if we should use outer or inner display on foldable devices based on a several
+ * inputs like device orientation, hinge angle signals.
+ *
+ * This is a stateful class and acts like a state machine with fixed number of states
+ * and transitions. It allows to list all possible state transitions instead of performing
+ * imperative logic to make sure that we cover all scenarios and improve debuggability.
+ *
+ * See {@link BookStyleStateTransitions} for detailed description of the default behavior.
+ */
+public class BookStylePreferredScreenCalculator {
+
+    /**
+     * When calculating the new state we will re-calculate it until it settles down. We re-calculate
+     * it because the new state might trigger another state transition and this might happen
+     * several times. We don't want to have infinite loops in state calculation, so this value
+     * limits the number of such state transitions.
+     * For example, in the default configuration {@link BookStyleStateTransitions}, after each
+     * transition with 'set sticky flag' output it will perform a transition to a state without
+     * 'set sticky flag' output.
+     * We also have a unit test covering all possible states which checks that we don't have such
+     * states that could end up in an infinite transition. See sample test for the default
+     * transitions in {@link BookStyleClosedStateCalculatorTest}.
+     */
+    private static final int MAX_STATE_CHANGES = 16;
+
+    private State mState = new State(
+            /* stickyKeepOuterUntil90Degrees= */ false,
+            /* stickyKeepInnerUntil45Degrees= */ false,
+            PreferredScreen.INVALID);
+
+    private final List<StateTransition> mStateTransitions;
+
+    /**
+     * Creates BookStyleClosedStateCalculator
+     * @param stateTransitions list of all state transitions
+     */
+    public BookStylePreferredScreenCalculator(List<StateTransition> stateTransitions) {
+        mStateTransitions = stateTransitions;
+    }
+
+    /**
+     * Calculates updated {@link PreferredScreen} based on the current inputs and the current state.
+     * The calculation is done based on defined {@link StateTransition}s, it might perform
+     * multiple transitions until we settle down on a single state. Multiple transitions could be
+     * performed in case if {@link StateTransition} causes another update of the state.
+     * There is a limit of maximum {@link MAX_STATE_CHANGES} state transitions, after which
+     * this method will throw an {@link IllegalStateException}.
+     *
+     * @param angle current hinge angle
+     * @param likelyTentOrWedge true if the device is likely in tent or wedge mode
+     * @param likelyReverseWedge true if the device is likely in reverse wedge mode
+     * @return updated {@link PreferredScreen}
+     */
+    public PreferredScreen calculatePreferredScreen(HingeAngle angle, boolean likelyTentOrWedge,
+            boolean likelyReverseWedge) {
+        int attempts = 0;
+        State newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge);
+        while (attempts < MAX_STATE_CHANGES && !Objects.equals(mState, newState)) {
+            mState = newState;
+            newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge);
+            attempts++;
+        }
+
+        if (attempts >= MAX_STATE_CHANGES) {
+            throw new IllegalStateException(
+                    "Can't settle state " + mState + ", inputs: hingeAngle = " + angle
+                            + ", likelyTentOrWedge = " + likelyTentOrWedge
+                            + ", likelyReverseWedge = " + likelyReverseWedge);
+        }
+
+        final State oldState = mState;
+        mState = newState;
+
+        if (mState.mPreferredScreen == PreferredScreen.INVALID) {
+            throw new IllegalStateException(
+                    "Reached invalid state " + mState + ", inputs: hingeAngle = " + angle
+                            + ", likelyTentOrWedge = " + likelyTentOrWedge
+                            + ", likelyReverseWedge = " + likelyReverseWedge + ", old state: "
+                            + oldState);
+        }
+
+        return mState.mPreferredScreen;
+    }
+
+    /**
+     * Returns the current state of the calculator
+     */
+    public State getState() {
+        return mState;
+    }
+
+    private State calculateNewState(State current, HingeAngle hingeAngle, boolean likelyTentOrWedge,
+            boolean likelyReverseWedge) {
+        for (int i = 0; i < mStateTransitions.size(); i++) {
+            final State newState = mStateTransitions.get(i).tryTransition(hingeAngle,
+                    likelyTentOrWedge, likelyReverseWedge, current);
+            if (newState != null) {
+                return newState;
+            }
+        }
+
+        throw new IllegalArgumentException(
+                "Entry not found for state: " + current + ", hingeAngle = " + hingeAngle
+                        + ", likelyTentOrWedge = " + likelyTentOrWedge + ", likelyReverseWedge = "
+                        + likelyReverseWedge);
+    }
+
+    /**
+     * The angle between two halves of the foldable device in degrees. The angle is '0' when
+     * the device is fully closed and '180' when the device is fully open and flat.
+     */
+    public enum HingeAngle {
+        ANGLE_0,
+        ANGLE_0_TO_45,
+        ANGLE_45_TO_90,
+        ANGLE_90_TO_180
+    }
+
+    /**
+     * Resulting closed state of the device, where OPEN state indicates that the device should use
+     * the inner display and CLOSED means that it should use the outer (cover) screen.
+     */
+    public enum PreferredScreen {
+        INNER,
+        OUTER,
+        INVALID
+    }
+
+    /**
+     * Describes a state transition for the posture based active screen calculator
+     */
+    public static class StateTransition {
+        private final Input mInput;
+        private final State mOutput;
+
+        public StateTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+                boolean likelyReverseWedge,
+                boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees,
+                PreferredScreen preferredScreen, Boolean setStickyKeepOuterUntil90Degrees,
+                Boolean setStickyKeepInnerUntil45Degrees) {
+            mInput = new Input(hingeAngle, likelyTentOrWedge, likelyReverseWedge,
+                    stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees);
+            mOutput = new State(setStickyKeepOuterUntil90Degrees,
+                    setStickyKeepInnerUntil45Degrees, preferredScreen);
+        }
+
+        /**
+         * Returns true if the state transition is applicable for the given inputs
+         */
+        private boolean isApplicable(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+                boolean likelyReverseWedge, State currentState) {
+            return mInput.hingeAngle == hingeAngle
+                    && mInput.likelyTentOrWedge == likelyTentOrWedge
+                    && mInput.likelyReverseWedge == likelyReverseWedge
+                    && Objects.equals(mInput.stickyKeepOuterUntil90Degrees,
+                    currentState.stickyKeepOuterUntil90Degrees)
+                    && Objects.equals(mInput.stickyKeepInnerUntil45Degrees,
+                    currentState.stickyKeepInnerUntil45Degrees);
+        }
+
+        /**
+         * Try to perform transition for the inputs, returns new state if this
+         * transition is applicable for the given state and inputs
+         */
+        @Nullable
+        State tryTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+                boolean likelyReverseWedge, State currentState) {
+            if (!isApplicable(hingeAngle, likelyTentOrWedge, likelyReverseWedge, currentState)) {
+                return null;
+            }
+
+            boolean stickyKeepOuterUntil90Degrees = currentState.stickyKeepOuterUntil90Degrees;
+            boolean stickyKeepInnerUntil45Degrees = currentState.stickyKeepInnerUntil45Degrees;
+
+            if (mOutput.stickyKeepOuterUntil90Degrees != null) {
+                stickyKeepOuterUntil90Degrees =
+                        mOutput.stickyKeepOuterUntil90Degrees;
+            }
+
+            if (mOutput.stickyKeepInnerUntil45Degrees != null) {
+                stickyKeepInnerUntil45Degrees =
+                        mOutput.stickyKeepInnerUntil45Degrees;
+            }
+
+            return new State(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees,
+                    mOutput.mPreferredScreen);
+        }
+    }
+
+    /**
+     * The input part of the {@link StateTransition}, these are the values that are used
+     * to decide which {@link State} output to choose.
+     */
+    private static class Input {
+        final HingeAngle hingeAngle;
+        final boolean likelyTentOrWedge;
+        final boolean likelyReverseWedge;
+        final boolean stickyKeepOuterUntil90Degrees;
+        final boolean stickyKeepInnerUntil45Degrees;
+
+        public Input(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+                boolean likelyReverseWedge,
+                boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees) {
+            this.hingeAngle = hingeAngle;
+            this.likelyTentOrWedge = likelyTentOrWedge;
+            this.likelyReverseWedge = likelyReverseWedge;
+            this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees;
+            this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof Input)) return false;
+            Input that = (Input) o;
+            return likelyTentOrWedge == that.likelyTentOrWedge
+                    && likelyReverseWedge == that.likelyReverseWedge
+                    && stickyKeepOuterUntil90Degrees == that.stickyKeepOuterUntil90Degrees
+                    && stickyKeepInnerUntil45Degrees == that.stickyKeepInnerUntil45Degrees
+                    && hingeAngle == that.hingeAngle;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(hingeAngle, likelyTentOrWedge, likelyReverseWedge,
+                    stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees);
+        }
+
+        @Override
+        public String toString() {
+            return "InputState{" +
+                    "hingeAngle=" + hingeAngle +
+                    ", likelyTentOrWedge=" + likelyTentOrWedge +
+                    ", likelyReverseWedge=" + likelyReverseWedge +
+                    ", stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees +
+                    ", stickyKeepInnerUntil45Degrees=" + stickyKeepInnerUntil45Degrees +
+                    '}';
+        }
+    }
+
+    /**
+     * Class that holds a state of the calculator, it could be used to store the current
+     * state or to define the target (output) state based on some input in {@link StateTransition}.
+     */
+    public static class State {
+        public Boolean stickyKeepOuterUntil90Degrees;
+        public Boolean stickyKeepInnerUntil45Degrees;
+
+        PreferredScreen mPreferredScreen;
+
+        public State(Boolean stickyKeepOuterUntil90Degrees,
+                Boolean stickyKeepInnerUntil45Degrees,
+                PreferredScreen preferredScreen) {
+            this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees;
+            this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees;
+            this.mPreferredScreen = preferredScreen;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof State)) return false;
+            State that = (State) o;
+            return Objects.equals(stickyKeepOuterUntil90Degrees,
+                    that.stickyKeepOuterUntil90Degrees) && Objects.equals(
+                    stickyKeepInnerUntil45Degrees, that.stickyKeepInnerUntil45Degrees)
+                    && mPreferredScreen == that.mPreferredScreen;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees,
+                    mPreferredScreen);
+        }
+
+        @Override
+        public String toString() {
+            return "State{" +
+                    "stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees +
+                    ", stickyKeepInnerUntil90Degrees=" + stickyKeepInnerUntil45Degrees +
+                    ", closedState=" + mPreferredScreen +
+                    '}';
+        }
+    }
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
new file mode 100644
index 0000000..16daacb
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
@@ -0,0 +1,722 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Describes all possible state transitions for {@link BookStylePreferredScreenCalculator}.
+ * It contains a default configuration for a foldable device that has two screens: smaller outer
+ * screen which has portrait natural orientation and a larger inner screen and allows to use the
+ * device in tent mode or wedge mode.
+ *
+ * As the output state could affect calculating of the new state, it could potentially cause
+ * infinite loop and make the state never settle down. This could be avoided using automated test
+ * that checks all possible inputs and asserts that the final state is valid.
+ * See sample test for the default transitions in {@link BookStyleClosedStateCalculatorTest}.
+ *
+ * - Tent mode is defined as a posture when the device is partially opened and placed on the ground
+ *   on the edges that are parallel to the hinge.
+ * - Wedge mode is when the device is partially opened and placed flat on the ground with the part
+ *   of the device that doesn't have the display
+ * - Reverse wedge mode is when the device is partially opened and placed flat on the ground with
+ *   the outer screen down, so the outer screen is not accessible
+ *
+ * Behavior description:
+ * - When unfolding with screens off we assume that no sensor data available except hinge angle
+ *   (based on hall sensor), so we switch to the inner screen immediately
+ *
+ * - When unfolding when screen is 'on' we can check if we are likely in tent or wedge mode
+ *   - If not likely tent/wedge mode or sensors data not available, then we unfold immediately
+ *     After unfolding, the state of the inner screen 'on' is sticky between 0 and 45 degrees, so
+ *     it won't jump back to the outer screen even if you move the phone into tent/wedge mode. The
+ *     stickiness is reset after fully closing the device or unfolding past 45 degrees.
+ *   - If likely tent or wedge mode, switch only at 90 degrees
+ *     Tent/wedge mode is 'sticky' between 0 and 90 degrees, so it won't reset until you either
+ *     fully close the device or unfold past 90 degrees.
+ *
+ * - When folding we can check if we are likely in reverse wedge mode
+ *   - If not likely in reverse wedge mode or sensor data is not available we switch to the outer
+ *     screen at 45 degrees and enable sticky tent/wedge mode as before, this allows to enter
+ *     tent/wedge mode even if you are not on an even surface or holding phone in landscape
+ *   - If likely in reverse wedge mode, switch to the outer screen only at 0 degrees to allow
+ *     some use cases like using camera in this posture, the check happens after passing 45 degrees
+ *     and inner screen becomes sticky turned 'on' until fully closing or unfolding past 45 degrees
+ */
+public class BookStyleStateTransitions {
+
+    public static final List<StateTransition> DEFAULT_STATE_TRANSITIONS = new ArrayList<>();
+
+    static {
+        // region Angle 0
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        // endregion
+
+        // region Angle 0-45
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ true,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ true,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        // endregion
+
+        // region Angle 45-90
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        // endregion
+
+        // region Angle 90-180
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        // endregion
+    }
+}
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
new file mode 100644
index 0000000..8d01b7a
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
@@ -0,0 +1,703 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.STATE_OFF;
+import static android.view.Display.STATE_ON;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Instrumentation;
+import android.content.res.Configuration;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.input.InputSensorInfo;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.devicestate.DeviceStateProvider.Listener;
+import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
+import com.android.server.policy.feature.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.FieldSetter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link BookStyleDeviceStatePolicy.Provider}.
+ * <p/>
+ * Run with <code>atest BookStyleDeviceStatePolicyTest</code>.
+ */
+@RunWith(AndroidTestingRunner.class)
+public final class BookStyleDeviceStatePolicyTest {
+
+    private static final int DEVICE_STATE_CLOSED = 0;
+    private static final int DEVICE_STATE_HALF_OPENED = 1;
+    private static final int DEVICE_STATE_OPENED = 2;
+
+    @Captor
+    private ArgumentCaptor<Integer> mDeviceStateCaptor;
+    @Captor
+    private ArgumentCaptor<DisplayManager.DisplayListener> mDisplayListenerCaptor;
+    @Mock
+    private SensorManager mSensorManager;
+    @Mock
+    private InputSensorInfo mInputSensorInfo;
+    @Mock
+    private Listener mListener;
+    @Mock
+    DisplayManager mDisplayManager;
+    @Mock
+    private Display mDisplay;
+
+    private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl();
+
+    private final Configuration mConfiguration = new Configuration();
+
+    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+    @Rule
+    public final TestableContext mContext = new TestableContext(
+            mInstrumentation.getTargetContext());
+
+    private Sensor mHallSensor;
+    private Sensor mOrientationSensor;
+    private Sensor mHingeAngleSensor;
+    private Sensor mLeftAccelerometer;
+    private Sensor mRightAccelerometer;
+
+    private Map<Sensor, List<SensorEventListener>> mSensorEventListeners = new HashMap<>();
+    private DeviceStateProvider mProvider;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_FOLDABLES_POSTURE_BASED_CLOSED_STATE, true);
+        mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true);
+
+        when(mInputSensorInfo.getName()).thenReturn("hall-effect");
+        mHallSensor = new Sensor(mInputSensorInfo);
+        when(mInputSensorInfo.getName()).thenReturn("hinge-angle");
+        mHingeAngleSensor = new Sensor(mInputSensorInfo);
+        when(mInputSensorInfo.getName()).thenReturn("left-accelerometer");
+        mLeftAccelerometer = new Sensor(mInputSensorInfo);
+        when(mInputSensorInfo.getName()).thenReturn("right-accelerometer");
+        mRightAccelerometer = new Sensor(mInputSensorInfo);
+        when(mInputSensorInfo.getName()).thenReturn("orientation");
+        mOrientationSensor = new Sensor(mInputSensorInfo);
+
+        mContext.addMockSystemService(SensorManager.class, mSensorManager);
+
+        when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_HINGE_ANGLE), eq(true)))
+                .thenReturn(mHingeAngleSensor);
+        when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_DEVICE_ORIENTATION)))
+                .thenReturn(mOrientationSensor);
+
+        when(mDisplayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(mDisplay);
+        mContext.addMockSystemService(DisplayManager.class, mDisplayManager);
+
+        mContext.ensureTestableResources();
+        when(mContext.getResources().getConfiguration()).thenReturn(mConfiguration);
+
+        final List<Sensor> sensors = new ArrayList<>();
+        sensors.add(mHallSensor);
+        sensors.add(mHingeAngleSensor);
+        sensors.add(mOrientationSensor);
+        sensors.add(mLeftAccelerometer);
+        sensors.add(mRightAccelerometer);
+
+        when(mSensorManager.registerListener(any(), any(), anyInt(), any())).thenAnswer(
+                invocation -> {
+                    final SensorEventListener listener = invocation.getArgument(0);
+                    final Sensor sensor = invocation.getArgument(1);
+                    addSensorListener(sensor, listener);
+                    return true;
+                });
+        when(mSensorManager.registerListener(any(), any(), anyInt())).thenAnswer(
+                invocation -> {
+                    final SensorEventListener listener = invocation.getArgument(0);
+                    final Sensor sensor = invocation.getArgument(1);
+                    addSensorListener(sensor, listener);
+                    return true;
+                });
+
+        doAnswer(invocation -> {
+            final SensorEventListener listener = invocation.getArgument(0);
+            final boolean[] removed = {false};
+            mSensorEventListeners.forEach((sensor, sensorEventListeners) ->
+                    removed[0] |= sensorEventListeners.remove(listener));
+
+            if (!removed[0]) {
+                throw new IllegalArgumentException(
+                        "Trying to unregister listener " + listener + " that was not registered");
+            }
+
+            return null;
+        }).when(mSensorManager).unregisterListener(any(SensorEventListener.class));
+
+        doAnswer(invocation -> {
+            final SensorEventListener listener = invocation.getArgument(0);
+            final Sensor sensor = invocation.getArgument(1);
+
+            boolean removed = mSensorEventListeners.get(sensor).remove(listener);
+            if (!removed) {
+                throw new IllegalArgumentException(
+                        "Trying to unregister listener " + listener
+                                + " that was not registered for sensor " + sensor);
+            }
+
+            return null;
+        }).when(mSensorManager).unregisterListener(any(SensorEventListener.class),
+                any(Sensor.class));
+
+        try {
+            FieldSetter.setField(mHallSensor, mHallSensor.getClass()
+                    .getDeclaredField("mStringType"), "com.google.sensor.hall_effect");
+        } catch (NoSuchFieldException e) {
+            throw new RuntimeException(e);
+        }
+
+        when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL)))
+                .thenReturn(sensors);
+
+        mInstrumentation.runOnMainSync(() -> mProvider = createProvider());
+
+        verify(mDisplayManager, atLeastOnce()).registerDisplayListener(
+                mDisplayListenerCaptor.capture(), nullable(Handler.class));
+        setScreenOn(true);
+    }
+
+    @Test
+    public void test_noSensorEventsYet_reportOpenedState() {
+        mProvider.setListener(mListener);
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_deviceClosedSensorEventsBecameAvailable_reportsClosedState() {
+        mProvider.setListener(mListener);
+        clearInvocations(mListener);
+
+        sendHingeAngle(0f);
+
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hingeAngleClosed_reportsClosedState() {
+        sendHingeAngle(0f);
+
+        mProvider.setListener(mListener);
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hingeAngleFullyOpened_reportsOpenedState() {
+        sendHingeAngle(180f);
+
+        mProvider.setListener(mListener);
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_unfoldingFromClosedToFullyOpened_reportsOpenedEvent() {
+        sendHingeAngle(0f);
+        mProvider.setListener(mListener);
+        clearInvocations(mListener);
+
+        sendHingeAngle(180f);
+
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_foldingFromFullyOpenToFullyClosed_movesToClosedState() {
+        sendHingeAngle(180f);
+
+        sendHingeAngle(0f);
+
+        mProvider.setListener(mListener);
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_slowUnfolding_reportsEventsInOrder() {
+        sendHingeAngle(0f);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(5f);
+        sendHingeAngle(10f);
+        sendHingeAngle(60f);
+        sendHingeAngle(100f);
+        sendHingeAngle(180f);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_OPENED
+        );
+    }
+
+    @Test
+    public void test_slowFolding_reportsEventsInOrder() {
+        sendHingeAngle(180f);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(180f);
+        sendHingeAngle(100f);
+        sendHingeAngle(60f);
+        sendHingeAngle(10f);
+        sendHingeAngle(5f);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_OPENED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_CLOSED
+        );
+    }
+
+    @Test
+    public void test_hingeAngleOpen_screenOff_reportsHalfFolded() {
+        sendHingeAngle(0f);
+        setScreenOn(false);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(10f);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED
+        );
+    }
+
+    @Test
+    public void test_slowUnfoldingWithScreenOff_reportsEventsInOrder() {
+        sendHingeAngle(0f);
+        setScreenOn(false);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(5f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(10f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(60f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(100f);
+        sendHingeAngle(180f);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_OPENED
+        );
+    }
+
+    @Test
+    public void test_unfoldWithScreenOff_reportsHalfOpened() {
+        sendHingeAngle(0f);
+        setScreenOn(false);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(5f);
+        sendHingeAngle(10f);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED
+        );
+    }
+
+    @Test
+    public void test_slowUnfoldingAndFolding_reportsEventsInOrder() {
+        sendHingeAngle(0f);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+        // Started unfolding
+        sendHingeAngle(5f);
+        sendHingeAngle(30f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(60f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(100f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(180f);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        // Started folding
+        sendHingeAngle(100f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(60f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(30f);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(5f);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_OPENED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_CLOSED
+        );
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_screenOnRightSideMostlyFlat_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(true);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_seascapeDeviceOrientation_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        sendDeviceOrientation(Surface.ROTATION_270);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_landscapeScreenRotation_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        sendScreenRotation(Surface.ROTATION_90);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_seascapeScreenRotation_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        sendScreenRotation(Surface.ROTATION_270);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_screenOnRightSideNotFlat_switchesToHalfOpenState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED);
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_screenOffRightSideFlat_switchesToHalfOpenState() {
+        sendHingeAngle(0f);
+        setScreenOn(false);
+        // This sensor event should be ignored as screen is off
+        sendRightSideFlatSensorEvent(true);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED);
+    }
+
+    @Test
+    public void test_unfoldTo60Degrees_andFoldTo10_switchesToClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(60f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(10f);
+
+        verify(mListener).onStateChanged(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_foldTo10AndUnfoldTo85Degrees_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(180f);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+        sendHingeAngle(10f);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+        sendHingeAngle(85f);
+
+        // Keeps 'tent'/'wedge' mode even when right side is not flat
+        // as user manually folded the device not all the way
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_foldTo0AndUnfoldTo85Degrees_doesNotKeepClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(180f);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+        sendHingeAngle(0f);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+        sendHingeAngle(85f);
+
+        // Do not enter 'tent'/'wedge' mode when right side is not flat
+        // as user fully folded the device before that
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+    }
+
+    @Test
+    public void test_foldTo10_leftSideIsFlat_keepsInnerScreenForReverseWedge() {
+        sendHingeAngle(180f);
+        sendLeftSideFlatSensorEvent(true);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        sendHingeAngle(10f);
+
+        // Keep the inner screen for reverse wedge mode (e.g. for astrophotography use case)
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+    }
+
+    @Test
+    public void test_foldTo10_leftSideIsNotFlat_switchesToOuterScreen() {
+        sendHingeAngle(180f);
+        sendLeftSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        sendHingeAngle(10f);
+
+        // Do not keep the inner screen as it is not reverse wedge mode
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_foldTo10_noAccelerometerEvents_switchesToOuterScreen() {
+        sendHingeAngle(180f);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        sendHingeAngle(10f);
+
+        // Do not keep the inner screen as it is not reverse wedge mode
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_deviceClosed_screenIsOff_noSensorListeners() {
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(0f);
+        setScreenOn(false);
+
+        assertNoListenersForSensor(mLeftAccelerometer);
+        assertNoListenersForSensor(mRightAccelerometer);
+        assertNoListenersForSensor(mOrientationSensor);
+    }
+
+    @Test
+    public void test_deviceClosed_screenIsOn_doesNotListenForOneAccelerometer() {
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(0f);
+        setScreenOn(true);
+
+        assertNoListenersForSensor(mLeftAccelerometer);
+        assertListensForSensor(mRightAccelerometer);
+        assertListensForSensor(mOrientationSensor);
+    }
+
+    @Test
+    public void test_deviceOpened_screenIsOn_listensToSensors() {
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(180f);
+        setScreenOn(true);
+
+        assertListensForSensor(mLeftAccelerometer);
+        assertListensForSensor(mRightAccelerometer);
+        assertListensForSensor(mOrientationSensor);
+    }
+
+    private void assertLatestReportedState(int state) {
+        final ArgumentCaptor<Integer> integerCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mListener, atLeastOnce()).onStateChanged(integerCaptor.capture());
+        assertEquals(state, integerCaptor.getValue().intValue());
+    }
+
+    private void sendHingeAngle(float angle) {
+        sendSensorEvent(mHingeAngleSensor, new float[]{angle});
+    }
+
+    private void sendDeviceOrientation(int orientation) {
+        sendSensorEvent(mOrientationSensor, new float[]{orientation});
+    }
+
+    private void sendScreenRotation(int rotation) {
+        when(mDisplay.getRotation()).thenReturn(rotation);
+        mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY));
+    }
+
+    private void sendRightSideFlatSensorEvent(boolean flat) {
+        sendAccelerometerFlatEvents(mRightAccelerometer, flat);
+    }
+
+    private void sendLeftSideFlatSensorEvent(boolean flat) {
+        sendAccelerometerFlatEvents(mLeftAccelerometer, flat);
+    }
+
+    private static final int ACCELEROMETER_EVENTS = 10;
+
+    private void sendAccelerometerFlatEvents(Sensor sensor, boolean flat) {
+        final float[] values = flat ? new float[]{0.00021f, -0.00013f, 9.7899f} :
+                new float[]{6.124f, 4.411f, -1.7899f};
+        // Send the same values multiple times to bypass noise filter
+        for (int i = 0; i < ACCELEROMETER_EVENTS; i++) {
+            sendSensorEvent(sensor, values);
+        }
+    }
+
+    private void setScreenOn(boolean isOn) {
+        int state = isOn ? STATE_ON : STATE_OFF;
+        when(mDisplay.getState()).thenReturn(state);
+        mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY));
+    }
+
+    private void sendSensorEvent(Sensor sensor, float[] values) {
+        SensorEvent event = mock(SensorEvent.class);
+        event.sensor = sensor;
+        try {
+            FieldSetter.setField(event, event.getClass().getField("values"),
+                    values);
+        } catch (NoSuchFieldException e) {
+            throw new RuntimeException(e);
+        }
+
+        List<SensorEventListener> listeners = mSensorEventListeners.get(sensor);
+        if (listeners != null) {
+            listeners.forEach(sensorEventListener -> sensorEventListener.onSensorChanged(event));
+        }
+    }
+
+    private void assertNoListenersForSensor(Sensor sensor) {
+        final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor,
+                new ArrayList<>());
+        assertWithMessage("Expected no listeners for sensor " + sensor + " but found some").that(
+                listeners).isEmpty();
+    }
+
+    private void assertListensForSensor(Sensor sensor) {
+        final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor,
+                new ArrayList<>());
+        assertWithMessage(
+                "Expected at least one listener for sensor " + sensor).that(
+                listeners).isNotEmpty();
+    }
+
+    private void addSensorListener(Sensor sensor, SensorEventListener listener) {
+        List<SensorEventListener> listeners = mSensorEventListeners.computeIfAbsent(
+                sensor, k -> new ArrayList<>());
+        listeners.add(listener);
+    }
+
+    private DeviceStateProvider createProvider() {
+        return new BookStyleDeviceStatePolicy(mFakeFeatureFlags, mContext, mHingeAngleSensor,
+                mHallSensor, mLeftAccelerometer, mRightAccelerometer,
+                /* closeAngleDegrees= */ null).getDeviceStateProvider();
+    }
+}
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java
new file mode 100644
index 0000000..ae05b3f
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+
+import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.testing.AndroidTestingRunner;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+
+import com.google.common.collect.Lists;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link BookStylePreferredScreenCalculator}.
+ * <p/>
+ * Run with <code>atest BookStyleClosedStateCalculatorTest</code>.
+ */
+@RunWith(AndroidTestingRunner.class)
+public final class BookStylePreferredScreenCalculatorTest {
+
+    private final BookStylePreferredScreenCalculator mCalculator =
+            new BookStylePreferredScreenCalculator(DEFAULT_STATE_TRANSITIONS);
+
+    private final List<HingeAngle> mHingeAngleValues = Arrays.asList(HingeAngle.values());
+    private final List<Boolean> mLikelyTentModeValues = Arrays.asList(true, false);
+    private final List<Boolean> mLikelyReverseWedgeModeValues = Arrays.asList(true, false);
+
+    @Test
+    public void transitionAllStates_noCrashes() {
+        final List<List<Object>> arguments = Lists.cartesianProduct(Arrays.asList(
+                mHingeAngleValues,
+                mLikelyTentModeValues,
+                mLikelyReverseWedgeModeValues
+        ));
+
+        arguments.forEach(objects -> {
+            final HingeAngle hingeAngle = (HingeAngle) objects.get(0);
+            final boolean likelyTent = (boolean) objects.get(1);
+            final boolean likelyReverseWedge = (boolean) objects.get(2);
+
+            final String description =
+                    "Input: hinge angle = " + hingeAngle + ", likelyTent = " + likelyTent
+                            + ", likelyReverseWedge = " + likelyReverseWedge;
+
+            // Verify that there are no crashes because of infinite state transitions and
+            // that it returns a valid active state
+            try {
+                PreferredScreen preferredScreen = mCalculator.calculatePreferredScreen(hingeAngle, likelyTent,
+                        likelyReverseWedge);
+
+                assertWithMessage(description).that(preferredScreen).isNotEqualTo(PreferredScreen.INVALID);
+            } catch (Throwable exception) {
+                throw new AssertionError(description, exception);
+            }
+        });
+    }
+}
diff --git a/services/profcollect/Android.bp b/services/profcollect/Android.bp
index 2040bb6..fe431f5 100644
--- a/services/profcollect/Android.bp
+++ b/services/profcollect/Android.bp
@@ -22,24 +22,25 @@
 }
 
 filegroup {
-  name: "services.profcollect-javasources",
-  srcs: ["src/**/*.java"],
-  path: "src",
-  visibility: ["//frameworks/base/services"],
+    name: "services.profcollect-javasources",
+    srcs: ["src/**/*.java"],
+    path: "src",
+    visibility: ["//frameworks/base/services"],
 }
 
 filegroup {
-  name: "services.profcollect-sources",
-  srcs: [
-    ":services.profcollect-javasources",
-    ":profcollectd_aidl",
-  ],
-  visibility: ["//frameworks/base/services:__subpackages__"],
+    name: "services.profcollect-sources",
+    srcs: [
+        ":services.profcollect-javasources",
+        ":profcollectd_aidl",
+    ],
+    visibility: ["//frameworks/base/services:__subpackages__"],
 }
 
 java_library_static {
-  name: "services.profcollect",
-  defaults: ["platform_service_defaults"],
-  srcs: [":services.profcollect-sources"],
-  libs: ["services.core"],
+    name: "services.profcollect",
+    defaults: ["platform_service_defaults"],
+    srcs: [":services.profcollect-sources"],
+    static_libs: ["services.core"],
+    libs: ["service-art.stubs.system_server"],
 }
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index 4007672..582b712 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -41,12 +41,15 @@
 import com.android.internal.R;
 import com.android.internal.os.BackgroundThread;
 import com.android.server.IoThread;
+import com.android.server.LocalManagerRegistry;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.art.ArtManagerLocal;
 import com.android.server.wm.ActivityMetricsLaunchObserver;
 import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
+import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 
@@ -261,6 +264,7 @@
         BackgroundThread.get().getThreadHandler().post(
                 () -> {
                     registerAppLaunchObserver();
+                    registerDex2oatObserver();
                     registerOTAObserver();
                 });
     }
@@ -304,6 +308,44 @@
         }
     }
 
+    private void registerDex2oatObserver() {
+        ArtManagerLocal aml = LocalManagerRegistry.getManager(ArtManagerLocal.class);
+        if (aml == null) {
+            Log.w(LOG_TAG, "Couldn't get ArtManagerLocal");
+            return;
+        }
+        aml.setBatchDexoptStartCallback(ForkJoinPool.commonPool(),
+                (snapshot, reason, defaultPackages, builder, passedSignal) -> {
+                    traceOnDex2oatStart();
+                });
+    }
+
+    private void traceOnDex2oatStart() {
+        if (mIProfcollect == null) {
+            return;
+        }
+        // Sample for a fraction of dex2oat runs.
+        final int traceFrequency =
+            DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
+                "dex2oat_trace_freq", 10);
+        int randomNum = ThreadLocalRandom.current().nextInt(100);
+        if (randomNum < traceFrequency) {
+            if (DEBUG) {
+                Log.d(LOG_TAG, "Tracing on dex2oat event");
+            }
+            BackgroundThread.get().getThreadHandler().post(() -> {
+                try {
+                    // Dex2oat could take a while before it starts. Add a short delay before start
+                    // tracing.
+                    Thread.sleep(1000);
+                    mIProfcollect.trace_once("dex2oat");
+                } catch (RemoteException | InterruptedException e) {
+                    Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
+                }
+            });
+        }
+    }
+
     private void registerOTAObserver() {
         UpdateEngine updateEngine = new UpdateEngine();
         updateEngine.bind(new UpdateEngineCallback() {
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
index 3c8f5c9..30afa72 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
@@ -15,9 +15,16 @@
  */
 package com.android.server.inputmethod;
 
+import static com.android.server.inputmethod.ClientController.ClientControllerCallback;
+import static com.android.server.inputmethod.ClientController.ClientState;
+
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.pm.PackageManagerInternal;
@@ -38,6 +45,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 // This test is designed to run on both device and host (Ravenwood) side.
 public final class ClientControllerTest {
@@ -58,9 +67,6 @@
     @Mock
     private IRemoteInputConnection mConnection;
 
-    @Mock
-    private IBinder.DeathRecipient mDeathRecipient;
-
     private Handler mHandler;
 
     private ClientController mController;
@@ -68,9 +74,10 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        when(mClient.asBinder()).thenReturn((IBinder) mClient);
+
         mHandler = new Handler(Looper.getMainLooper());
         mController = new ClientController(mMockPackageManagerInternal);
-        when(mClient.asBinder()).thenReturn((IBinder) mClient);
     }
 
     @Test
@@ -80,18 +87,77 @@
         var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
 
         synchronized (ImfLock.class) {
-            mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, mDeathRecipient,
-                    ANY_CALLER_UID, ANY_CALLER_PID);
+            mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+                    ANY_CALLER_PID);
 
             SecurityException thrown = assertThrows(SecurityException.class,
                     () -> {
                         synchronized (ImfLock.class) {
                             mController.addClient(invoker, mConnection, ANY_DISPLAY_ID,
-                                    mDeathRecipient, ANY_CALLER_UID, ANY_CALLER_PID);
+                                    ANY_CALLER_UID, ANY_CALLER_PID);
                         }
                     });
             assertThat(thrown.getMessage()).isEqualTo(
                     "uid=1/pid=1/displayId=0 is already registered");
         }
     }
+
+    @Test
+    // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed.
+    @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
+    public void testAddClient() throws Exception {
+        synchronized (ImfLock.class) {
+            var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
+            var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+                    ANY_CALLER_PID);
+
+            verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0));
+            assertThat(mController.mClients).containsEntry(invoker.asBinder(), added);
+        }
+    }
+
+    @Test
+    // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed.
+    @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
+    public void testRemoveClient() {
+        var callback = new TestClientControllerCallback();
+        ClientState added;
+        synchronized (ImfLock.class) {
+            mController.addClientControllerCallback(callback);
+
+            var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
+            added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+                    ANY_CALLER_PID);
+            assertThat(mController.mClients).containsEntry(invoker.asBinder(), added);
+            assertThat(mController.removeClient(mClient)).isTrue();
+        }
+
+        // Test callback
+        var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS);
+        assertThat(removed).isSameInstanceAs(added);
+    }
+
+    private static class TestClientControllerCallback implements ClientControllerCallback {
+
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+
+        private ClientState mRemoved;
+
+        @Override
+        public void onClientRemoved(ClientState removed) {
+            mRemoved = removed;
+            mLatch.countDown();
+        }
+
+        ClientState waitForRemovedClient(long timeout, TimeUnit unit) {
+            try {
+                assertWithMessage("ClientController callback wasn't called on user removed").that(
+                        mLatch.await(timeout, unit)).isTrue();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IllegalStateException("Unexpected thread interruption", e);
+            }
+            return mRemoved;
+        }
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index c67e7c5..b29fc88 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -824,6 +824,16 @@
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
                         AUTO_BRIGHTNESS_MODE_DOZE,
                         Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), SMALL_DELTA);
+
+        // Should fall back to the normal preset
+        assertArrayEquals(new float[]{0.0f, 95},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_DOZE,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), ZERO_DELTA);
+        assertArrayEquals(new float[]{0.35f, 0.45f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_DOZE,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), SMALL_DELTA);
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
new file mode 100644
index 0000000..638924e
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.hardware.display.BrightnessInfo
+import android.view.Display
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.feature.DisplayManagerFlags
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class BrightnessObserverTest {
+
+    @get:Rule
+    val mockitoRule = MockitoJUnit.rule()
+
+    private lateinit var spyContext: Context
+    private val mockInjector = mock<DisplayModeDirector.Injector>()
+    private val mockFlags = mock<DisplayManagerFlags>()
+    private val mockDeviceConfig = mock<DisplayDeviceConfig>()
+
+    private val testHandler = TestHandler(null)
+
+    @Before
+    fun setUp() {
+        spyContext = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+    }
+
+    @Test
+    fun testLowLightBlockingZoneVotes(@TestParameter testCase: LowLightTestCase) {
+        setUpLowBrightnessZone()
+        whenever(mockFlags.isVsyncLowLightVoteEnabled).thenReturn(testCase.vsyncLowLightVoteEnabled)
+        val displayModeDirector = DisplayModeDirector(
+                spyContext, testHandler, mockInjector, mockFlags)
+        val brightnessObserver = displayModeDirector.BrightnessObserver(
+                spyContext, testHandler, mockInjector, testCase.vrrSupported, mockFlags)
+
+        brightnessObserver.onRefreshRateSettingChangedLocked(0.0f, 120.0f)
+        brightnessObserver.updateBlockingZoneThresholds(mockDeviceConfig, false)
+        brightnessObserver.onDeviceConfigRefreshRateInLowZoneChanged(60)
+
+        brightnessObserver.onDisplayChanged(Display.DEFAULT_DISPLAY)
+
+        assertThat(displayModeDirector.getVote(VotesStorage.GLOBAL_ID,
+                Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH)).isEqualTo(testCase.expectedVote)
+    }
+
+    private fun setUpLowBrightnessZone() {
+        whenever(mockInjector.getBrightnessInfo(Display.DEFAULT_DISPLAY)).thenReturn(
+                BrightnessInfo(/* brightness = */ 0.05f, /* adjustedBrightness = */ 0.05f,
+                        /* brightnessMinimum = */ 0.0f, /* brightnessMaximum = */ 1.0f,
+                        BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                        /* highBrightnessTransitionPoint = */ 1.0f,
+                        BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE))
+        whenever(mockDeviceConfig.highDisplayBrightnessThresholds).thenReturn(floatArrayOf())
+        whenever(mockDeviceConfig.highAmbientBrightnessThresholds).thenReturn(floatArrayOf())
+        whenever(mockDeviceConfig.lowDisplayBrightnessThresholds).thenReturn(floatArrayOf(0.1f))
+        whenever(mockDeviceConfig.lowAmbientBrightnessThresholds).thenReturn(floatArrayOf(10f))
+    }
+
+    enum class LowLightTestCase(
+            val vrrSupported: Boolean,
+            val vsyncLowLightVoteEnabled: Boolean,
+            internal val expectedVote: Vote
+    ) {
+        ALL_ENABLED(true, true, CombinedVote(
+                listOf(DisableRefreshRateSwitchingVote(true),
+                        SupportedModesVote(
+                                listOf(SupportedModesVote.SupportedMode(60f, 60f),
+                                        SupportedModesVote.SupportedMode(120f, 120f)))))),
+        VRR_NOT_SUPPORTED(false, true, DisableRefreshRateSwitchingVote(true)),
+        VSYNC_VOTE_DISABLED(true, false, DisableRefreshRateSwitchingVote(true))
+    }
+}
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
index e2c338a..7e1dc08 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
@@ -202,7 +202,7 @@
         final ServiceInfo regularService = new ServiceInfo();
         regularService.processName = "com.foo";
         String processName = ActiveServices.getProcessNameForService(regularService, null, null,
-                null, false, false);
+                null, false, false, false);
         assertEquals("com.foo", processName);
 
         // Isolated service
@@ -211,29 +211,90 @@
         isolatedService.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
         final ComponentName component = new ComponentName("com.foo", "barService");
         processName = ActiveServices.getProcessNameForService(isolatedService, component,
-                null, null, false, false);
+                null, null, false, false, false);
         assertEquals("com.foo:barService", processName);
 
+        // Isolated Service in package private process.
+        final ServiceInfo isolatedService1 = new ServiceInfo();
+        isolatedService1.processName = "com.foo:trusted_isolated";
+        isolatedService1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+        final ComponentName componentName = new ComponentName("com.foo", "barService");
+        processName = ActiveServices.getProcessNameForService(isolatedService1, componentName,
+                null, null, false, false, false);
+        assertEquals("com.foo:trusted_isolated:barService", processName);
+
+        // Isolated service in package-private shared process (main process)
+        final ServiceInfo isolatedPackageSharedService = new ServiceInfo();
+        final ComponentName componentName1 = new ComponentName("com.foo", "barService");
+        isolatedPackageSharedService.processName = "com.foo";
+        isolatedPackageSharedService.applicationInfo = new ApplicationInfo();
+        isolatedPackageSharedService.applicationInfo.processName = "com.foo";
+        isolatedPackageSharedService.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+        String packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedPackageSharedService, componentName1, null, null, false, false, true);
+        assertEquals("com.foo:barService", packageSharedIsolatedProcessName);
+
+        // Isolated service in package-private shared process
+        final ServiceInfo isolatedPackageSharedService1 = new ServiceInfo(
+                isolatedPackageSharedService);
+        isolatedPackageSharedService1.processName = "com.foo:trusted_isolated";
+        isolatedPackageSharedService1.applicationInfo = new ApplicationInfo();
+        isolatedPackageSharedService1.applicationInfo.processName = "com.foo";
+        isolatedPackageSharedService1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+        packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedPackageSharedService1, componentName1, null, null, false, false, true);
+        assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+
+        // Bind another one in the same isolated process
+        final ServiceInfo isolatedPackageSharedService2 = new ServiceInfo(
+                isolatedPackageSharedService1);
+        packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedPackageSharedService2, componentName1, null, null, false, false, true);
+        assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+        // Simulate another app trying to do the bind.
+        final ServiceInfo isolatedPackageSharedService3 = new ServiceInfo(
+                isolatedPackageSharedService1);
+        final String auxCallingPackage = "com.bar";
+        packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedPackageSharedService3, componentName1, auxCallingPackage, null,
+                false, false, true);
+        assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+        // Simulate another app owning the service
+        final ServiceInfo isolatedOtherPackageSharedService = new ServiceInfo(
+                isolatedPackageSharedService1);
+        final ComponentName componentName2 = new ComponentName("com.bar", "barService");
+        isolatedOtherPackageSharedService.processName = "com.bar:isolated";
+        isolatedPackageSharedService.applicationInfo.processName = "com.bar";
+        final String mainCallingPackage = "com.foo";
+        packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedOtherPackageSharedService, componentName2, mainCallingPackage,
+                null, false, false, true);
+        assertEquals("com.bar:isolated", packageSharedIsolatedProcessName);
+
         // Isolated service in shared isolated process
         final ServiceInfo isolatedServiceShared1 = new ServiceInfo();
         isolatedServiceShared1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
         final String instanceName = "pool";
         final String callingPackage = "com.foo";
         final String sharedIsolatedProcessName1 = ActiveServices.getProcessNameForService(
-                isolatedServiceShared1, null, callingPackage, instanceName, false, true);
+                isolatedServiceShared1, null, callingPackage, instanceName, false, true, false);
         assertEquals("com.foo:ishared:pool", sharedIsolatedProcessName1);
 
         // Bind another one in the same isolated process
         final ServiceInfo isolatedServiceShared2 = new ServiceInfo(isolatedServiceShared1);
         final String sharedIsolatedProcessName2 = ActiveServices.getProcessNameForService(
-                isolatedServiceShared2, null, callingPackage, instanceName, false, true);
+                isolatedServiceShared2, null, callingPackage, instanceName, false, true, false);
         assertEquals(sharedIsolatedProcessName1, sharedIsolatedProcessName2);
 
         // Simulate another app trying to do the bind
         final ServiceInfo isolatedServiceShared3 = new ServiceInfo(isolatedServiceShared1);
         final String otherCallingPackage = "com.bar";
         final String sharedIsolatedProcessName3 = ActiveServices.getProcessNameForService(
-                isolatedServiceShared3, null, otherCallingPackage, instanceName, false, true);
+                isolatedServiceShared3, null, otherCallingPackage, instanceName, false, true,
+                false);
         Assert.assertNotEquals(sharedIsolatedProcessName2, sharedIsolatedProcessName3);
     }
 
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 650c473..116d5db 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
@@ -26,15 +26,18 @@
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
+import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
+import static com.android.server.job.controllers.FlexibilityController.FLEXIBLE_CONSTRAINTS;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_APPLIED_CONSTRAINTS;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_DEADLINE_PROXIMITY_LIMIT;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS;
-import static com.android.server.job.controllers.FlexibilityController.FLEXIBLE_CONSTRAINTS;
 import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
@@ -50,24 +53,32 @@
 import static org.junit.Assert.assertFalse;
 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.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AlarmManager;
 import android.app.AppGlobals;
 import android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.net.NetworkRequest;
 import android.os.Looper;
+import android.os.PowerManager;
 import android.provider.DeviceConfig;
 import android.util.ArraySet;
+import android.util.EmptyArray;
 
 import com.android.server.AppSchedulingModuleThread;
+import com.android.server.DeviceIdleInternal;
 import com.android.server.LocalServices;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.JobStore;
@@ -77,6 +88,7 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.MockitoSession;
@@ -95,6 +107,7 @@
     private static final long FROZEN_TIME = 100L;
 
     private MockitoSession mMockingSession;
+    private BroadcastReceiver mBroadcastReceiver;
     private FlexibilityController mFlexibilityController;
     private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
     private JobStore mJobStore;
@@ -106,6 +119,8 @@
     @Mock
     private Context mContext;
     @Mock
+    private DeviceIdleInternal mDeviceIdleInternal;
+    @Mock
     private JobSchedulerService mJobSchedulerService;
     @Mock
     private PrefetchController mPrefetchController;
@@ -128,10 +143,13 @@
         // Called in FlexibilityController constructor.
         when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
         when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
+        doNothing().when(mAlarmManager).setExact(anyInt(), anyLong(), anyString(), any(), any());
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
         when(mPackageManager.hasSystemFeature(
                 PackageManager.FEATURE_AUTOMOTIVE)).thenReturn(false);
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_EMBEDDED)).thenReturn(false);
+        doReturn(mDeviceIdleInternal)
+                .when(() -> LocalServices.getService(DeviceIdleInternal.class));
         // Used in FlexibilityController.FcConstants.
         doAnswer((Answer<Void>) invocationOnMock -> null)
                 .when(() -> DeviceConfig.addOnPropertiesChangedListener(
@@ -146,7 +164,7 @@
                         eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>any()));
         //used to get jobs by UID
         mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir());
-        when(mJobSchedulerService.getJobStore()).thenReturn(mJobStore);
+        doReturn(mJobStore).when(mJobSchedulerService).getJobStore();
         // Used in JobStatus.
         doReturn(mock(PackageManagerInternal.class))
                 .when(() -> LocalServices.getService(PackageManagerInternal.class));
@@ -156,6 +174,8 @@
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC);
         // Initialize real objects.
+        ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
         mFlexibilityController = new FlexibilityController(mJobSchedulerService,
                 mPrefetchController);
         mFcConfig = mFlexibilityController.getFcConfig();
@@ -166,6 +186,11 @@
         setDeviceConfigLong(KEY_DEADLINE_PROXIMITY_LIMIT, 0L);
         setDeviceConfigInt(KEY_APPLIED_CONSTRAINTS, FLEXIBLE_CONSTRAINTS);
         waitForQuietModuleThread();
+
+        verify(mContext).registerReceiver(receiverCaptor.capture(),
+                ArgumentMatchers.argThat(filter ->
+                        filter.hasAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED)));
+        mBroadcastReceiver = receiverCaptor.getValue();
     }
 
     @After
@@ -212,6 +237,7 @@
         JobStatus js = JobStatus.createFromJobInfo(
                 jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag);
         js.enqueueTime = FROZEN_TIME;
+        js.setStandbyBucket(ACTIVE_INDEX);
         if (js.hasFlexibilityConstraint()) {
             js.setNumAppliedFlexibleConstraints(Integer.bitCount(
                     mFlexibilityController.getRelevantAppliedConstraintsLocked(js)));
@@ -598,10 +624,10 @@
     @Test
     public void testGetLifeCycleBeginningElapsedLocked_Prefetch() {
         // prefetch with lifecycle
-        when(mPrefetchController.getLaunchTimeThresholdMs()).thenReturn(700L);
+        doReturn(700L).when(mPrefetchController).getLaunchTimeThresholdMs();
         JobInfo.Builder jb = createJob(0).setPrefetch(true);
         JobStatus js = createJobStatus("time", jb);
-        when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(900L);
+        doReturn(900L).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
         assertEquals(900L - 700L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
         // prefetch with enqueue
         jb = createJob(0).setPrefetch(true);
@@ -616,7 +642,7 @@
         // prefetch without estimate
         mFlexibilityController.mPrefetchLifeCycleStart
                 .add(js.getUserId(), js.getSourcePackageName(), 500L);
-        when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(Long.MAX_VALUE);
+        doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
         jb = createJob(0).setPrefetch(true);
         js = createJobStatus("time", jb);
         assertEquals(500L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
@@ -642,12 +668,12 @@
         // prefetch no estimate
         JobInfo.Builder jb = createJob(0).setPrefetch(true);
         JobStatus js = createJobStatus("time", jb);
-        when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(Long.MAX_VALUE);
+        doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
         assertEquals(Long.MAX_VALUE, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
         // prefetch with estimate
         jb = createJob(0).setPrefetch(true);
         js = createJobStatus("time", jb);
-        when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(1000L);
+        doReturn(1000L).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
         assertEquals(1000L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
     }
 
@@ -696,7 +722,7 @@
         // Stop satisfied constraints from causing a false positive.
         js.setNumAppliedFlexibleConstraints(100);
         synchronized (mFlexibilityController.mLock) {
-            when(mJobSchedulerService.isCurrentlyRunningLocked(js)).thenReturn(true);
+            doReturn(true).when(mJobSchedulerService).isCurrentlyRunningLocked(js);
             assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
         }
     }
@@ -847,14 +873,85 @@
     }
 
     @Test
+    public void testAllowlistedAppBypass() {
+        setPowerWhitelistExceptIdle();
+        mFlexibilityController.onSystemServicesReady();
+
+        JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
+        JobStatus jsDefault = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT));
+        JobStatus jsLow = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW));
+        JobStatus jsMin = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN));
+        jsHigh.setStandbyBucket(EXEMPTED_INDEX);
+        jsDefault.setStandbyBucket(EXEMPTED_INDEX);
+        jsLow.setStandbyBucket(EXEMPTED_INDEX);
+        jsMin.setStandbyBucket(EXEMPTED_INDEX);
+
+        setPowerWhitelistExceptIdle();
+        synchronized (mFlexibilityController.mLock) {
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+
+        setPowerWhitelistExceptIdle(SOURCE_PACKAGE);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+    }
+
+    @Test
+    public void testForegroundAppBypass() {
+        JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
+        JobStatus jsDefault = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT));
+        JobStatus jsLow = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW));
+        JobStatus jsMin = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN));
+
+        doReturn(JobInfo.BIAS_DEFAULT).when(mJobSchedulerService).getUidBias(mSourceUid);
+        synchronized (mFlexibilityController.mLock) {
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+
+        setUidBias(mSourceUid, JobInfo.BIAS_BOUND_FOREGROUND_SERVICE);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+
+        setUidBias(mSourceUid, JobInfo.BIAS_FOREGROUND_SERVICE);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+    }
+
+    @Test
     public void testTopAppBypass() {
-        JobInfo.Builder jb = createJob(0);
+        JobInfo.Builder jb = createJob(0).setPriority(JobInfo.PRIORITY_MIN);
         JobStatus js = createJobStatus("testTopAppBypass", jb);
         mJobStore.add(js);
 
         // Needed because if before and after Uid bias is the same, nothing happens.
         when(mJobSchedulerService.getUidBias(mSourceUid))
-                .thenReturn(JobInfo.BIAS_FOREGROUND_SERVICE);
+                .thenReturn(JobInfo.BIAS_DEFAULT);
 
         synchronized (mFlexibilityController.mLock) {
             mFlexibilityController.maybeStartTrackingJobLocked(js, null);
@@ -865,7 +962,7 @@
             assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
             assertTrue(js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE));
 
-            setUidBias(mSourceUid, JobInfo.BIAS_FOREGROUND_SERVICE);
+            setUidBias(mSourceUid, JobInfo.BIAS_SYNC_INITIALIZATION);
 
             assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
             assertFalse(js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE));
@@ -1187,9 +1284,9 @@
         JobInfo.Builder jb = createJob(22).setPrefetch(true);
         JobStatus js = createJobStatus("onPrefetchCacheUpdated", jb);
         jobs.add(js);
-        when(mPrefetchController.getLaunchTimeThresholdMs()).thenReturn(7 * HOUR_IN_MILLIS);
-        when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(
-                1150L + mFlexibilityController.mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS);
+        doReturn(7 * HOUR_IN_MILLIS).when(mPrefetchController).getLaunchTimeThresholdMs();
+        doReturn(1150L + mFlexibilityController.mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS)
+                .when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
 
         mFlexibilityController.maybeStartTrackingJobLocked(js, null);
 
@@ -1245,7 +1342,6 @@
         setUidBias(mSourceUid, BIAS_FOREGROUND_SERVICE);
         assertEquals(100L, (long) mFlexibilityController
                 .mPrefetchLifeCycleStart.get(js.getSourceUserId(), js.getSourcePackageName()));
-
     }
 
     @Test
@@ -1259,7 +1355,7 @@
     }
 
     private void runTestUnsupportedDevice(String feature) {
-        when(mPackageManager.hasSystemFeature(feature)).thenReturn(true);
+        doReturn(true).when(mPackageManager).hasSystemFeature(feature);
         mFlexibilityController =
                 new FlexibilityController(mJobSchedulerService, mPrefetchController);
         assertFalse(mFlexibilityController.isEnabled());
@@ -1279,6 +1375,16 @@
         }
     }
 
+    private void setPowerWhitelistExceptIdle(String... packages) {
+        doReturn(packages == null ? EmptyArray.STRING : packages)
+                .when(mDeviceIdleInternal).getFullPowerWhitelistExceptIdle();
+        if (mBroadcastReceiver != null) {
+            mBroadcastReceiver.onReceive(mContext,
+                    new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED));
+            waitForQuietModuleThread();
+        }
+    }
+
     private void setUidBias(int uid, int bias) {
         int prevBias = mJobSchedulerService.getUidBias(uid);
         doReturn(bias).when(mJobSchedulerService).getUidBias(uid);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java
index 1b9e6fb..a8eace0 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java
@@ -153,6 +153,15 @@
     }
 
     @Test
+    public void testGetIsHardwareIgnoringTouches() throws RemoteException {
+        mListener.onHardwareIgnoreTouchesChanged(true);
+        assertThat(mProvider.isHardwareIgnoringTouches()).isTrue();
+
+        mListener.onHardwareIgnoreTouchesChanged(false);
+        assertThat(mProvider.isHardwareIgnoringTouches()).isFalse();
+    }
+
+    @Test
     public void testGetDockedState() {
         final List<Integer> states = List.of(Intent.EXTRA_DOCK_STATE_DESK,
                 Intent.EXTRA_DOCK_STATE_CAR, Intent.EXTRA_DOCK_STATE_UNDOCKED);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricFrameworkStatsLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricFrameworkStatsLoggerTest.java
index 5cff48d..4119352 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricFrameworkStatsLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricFrameworkStatsLoggerTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.common.AuthenticateReason;
 import android.hardware.biometrics.common.OperationContext;
@@ -48,11 +49,13 @@
     public void testConvertsWakeReason_whenPowerReason() {
         final OperationContext context = new OperationContext();
         context.wakeReason = WakeReason.WAKE_MOTION;
-        final OperationContextExt ctx = new OperationContextExt(context, false);
+        final OperationContextExt ctx = new OperationContextExt(context, false,
+                BiometricAuthenticator.TYPE_NONE);
 
         final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx);
         final int[] reasonDetails = BiometricFrameworkStatsLogger
-                .toProtoWakeReasonDetails(new OperationContextExt(context, false));
+                .toProtoWakeReasonDetails(
+                        new OperationContextExt(context, false, BiometricAuthenticator.TYPE_NONE));
 
         assertThat(reason).isEqualTo(BiometricsProtoEnums.WAKE_REASON_WAKE_MOTION);
         assertThat(reasonDetails).isEmpty();
@@ -63,7 +66,8 @@
         final OperationContext context = new OperationContext();
         context.authenticateReason = AuthenticateReason.faceAuthenticateReason(
                 AuthenticateReason.Face.ASSISTANT_VISIBLE);
-        final OperationContextExt ctx = new OperationContextExt(context, false);
+        final OperationContextExt ctx = new OperationContextExt(context, false,
+                BiometricAuthenticator.TYPE_NONE);
 
         final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx);
         final int[] reasonDetails = BiometricFrameworkStatsLogger
@@ -79,7 +83,8 @@
         final OperationContext context = new OperationContext();
         context.authenticateReason = AuthenticateReason.vendorAuthenticateReason(
                 new AuthenticateReason.Vendor());
-        final OperationContextExt ctx = new OperationContextExt(context, false);
+        final OperationContextExt ctx = new OperationContextExt(context, false,
+                BiometricAuthenticator.TYPE_NONE);
 
         final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx);
         final int[] reasonDetails = BiometricFrameworkStatsLogger
@@ -96,7 +101,8 @@
         context.wakeReason = WakeReason.WAKE_KEY;
         context.authenticateReason = AuthenticateReason.faceAuthenticateReason(
                 AuthenticateReason.Face.PRIMARY_BOUNCER_SHOWN);
-        final OperationContextExt ctx = new OperationContextExt(context, false);
+        final OperationContextExt ctx = new OperationContextExt(context, false,
+                BiometricAuthenticator.TYPE_NONE);
 
         final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx);
         final int[] reasonDetails = BiometricFrameworkStatsLogger
@@ -113,7 +119,8 @@
         context.wakeReason = WakeReason.LID;
         context.authenticateReason = AuthenticateReason.vendorAuthenticateReason(
                 new AuthenticateReason.Vendor());
-        final OperationContextExt ctx = new OperationContextExt(context, false);
+        final OperationContextExt ctx = new OperationContextExt(context, false,
+                BiometricAuthenticator.TYPE_NONE);
 
         final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx);
         final int[] reasonDetails = BiometricFrameworkStatsLogger
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/OperationContextExtTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/OperationContextExtTest.java
index 32284fd..767b426 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/OperationContextExtTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/OperationContextExtTest.java
@@ -18,17 +18,19 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.when;
+
 import android.content.Intent;
 import android.hardware.biometrics.AuthenticateOptions;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.common.DisplayState;
 import android.hardware.biometrics.common.OperationContext;
 import android.hardware.biometrics.common.OperationReason;
+import android.hardware.biometrics.common.OperationState;
 import android.platform.test.annotations.Presubmit;
 import android.view.Surface;
 
-import static org.mockito.Mockito.when;
-
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.InstanceId;
@@ -58,7 +60,7 @@
 
         final OperationContext aidlContext = newAidlContext();
 
-        context = new OperationContextExt(aidlContext, false);
+        context = new OperationContextExt(aidlContext, false, BiometricAuthenticator.TYPE_NONE);
         assertThat(context.toAidlContext()).isSameInstanceAs(aidlContext);
 
         final int id = 5;
@@ -96,7 +98,8 @@
         );
 
         for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
-            final OperationContextExt context = new OperationContextExt(newAidlContext(), true);
+            final OperationContextExt context = new OperationContextExt(newAidlContext(), true,
+                    BiometricAuthenticator.TYPE_NONE);
             when(mBiometricContext.getDisplayState()).thenReturn(entry.getKey());
             assertThat(context.update(mBiometricContext, context.isCrypto()).getDisplayState())
                     .isEqualTo(entry.getValue());
@@ -124,7 +127,7 @@
         updatesFromSource(null, OperationReason.UNKNOWN);
     }
 
-    private  void updatesFromSource(BiometricContextSessionInfo sessionInfo, int sessionType) {
+    private void updatesFromSource(BiometricContextSessionInfo sessionInfo, int sessionType) {
         final int rotation = Surface.ROTATION_270;
         final int foldState = IBiometricContextListener.FoldState.HALF_OPENED;
         final int dockState = Intent.EXTRA_DOCK_STATE_CAR;
@@ -135,9 +138,11 @@
         when(mBiometricContext.getDockedState()).thenReturn(dockState);
         when(mBiometricContext.isDisplayOn()).thenReturn(true);
         when(mBiometricContext.getDisplayState()).thenReturn(displayState);
+        when(mBiometricContext.isHardwareIgnoringTouches()).thenReturn(true);
 
         final OperationContextExt context = new OperationContextExt(newAidlContext(),
-                sessionType == OperationReason.BIOMETRIC_PROMPT);
+                sessionType == OperationReason.BIOMETRIC_PROMPT,
+                BiometricAuthenticator.TYPE_FINGERPRINT);
 
         assertThat(context.update(mBiometricContext, context.isCrypto())).isSameInstanceAs(context);
 
@@ -154,6 +159,46 @@
         assertThat(context.getOrientation()).isEqualTo(rotation);
         assertThat(context.isDisplayOn()).isTrue();
         assertThat(context.getDisplayState()).isEqualTo(DisplayState.AOD);
+        assertThat(
+            context.getOperationState().getFingerprintOperationState().isHardwareIgnoringTouches
+        ).isTrue();
+    }
+
+    @Test
+    public void hasNullOperationState() {
+        OperationContextExt context = new OperationContextExt(false);
+        assertThat(context.toAidlContext()).isNotNull();
+
+        final OperationContext aidlContext = newAidlContext();
+
+        context = new OperationContextExt(aidlContext, false, BiometricAuthenticator.TYPE_NONE);
+        assertThat(context.getOperationState()).isNull();
+    }
+
+    @Test
+    public void hasFaceOperationState() {
+        OperationContextExt context = new OperationContextExt(false);
+        assertThat(context.toAidlContext()).isNotNull();
+
+        final OperationContext aidlContext = newAidlContext();
+
+        context = new OperationContextExt(aidlContext, false,
+                BiometricAuthenticator.TYPE_FACE);
+        assertThat(context.getOperationState().getTag()).isEqualTo(
+                OperationState.faceOperationState);
+    }
+
+    @Test
+    public void hasFingerprintOperationState() {
+        OperationContextExt context = new OperationContextExt(false);
+        assertThat(context.toAidlContext()).isNotNull();
+
+        final OperationContext aidlContext = newAidlContext();
+
+        context = new OperationContextExt(aidlContext, false,
+                BiometricAuthenticator.TYPE_FINGERPRINT);
+        assertThat(context.getOperationState().getTag()).isEqualTo(
+                OperationState.fingerprintOperationState);
     }
 
     private static OperationContext newAidlContext() {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java
index 2d9d868..4604b31 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java
@@ -72,6 +72,7 @@
                 mToken, mClientCallback);
         client.start(mSchedulerCallback);
         assertTrue(client.mHalOperationRunning);
+        verify(mClientCallback).getModality();
         verify(mSchedulerCallback).onClientStarted(eq(client));
 
         // Pretend that it got canceled by the user.
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index e1f490a..d71844b 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -608,6 +608,20 @@
     }
 
     @Test
+    public void testIsInputDeviceOwnedByVirtualDevice() {
+        assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isFalse();
+
+        final int fd = 1;
+        mInputController.addDeviceForTesting(BINDER, fd,
+                InputController.InputDeviceDescriptor.TYPE_KEYBOARD, DISPLAY_ID_1, PHYS,
+                DEVICE_NAME_1, INPUT_DEVICE_ID);
+        assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isTrue();
+
+        mInputController.unregisterInputDevice(BINDER);
+        assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isFalse();
+    }
+
+    @Test
     public void getDeviceIdsForUid_noRunningApps_returnsNull() {
         assertThat(mLocalService.getDeviceIdsForUid(UID_1)).isEmpty();
         assertThat(mVdmNative.getDeviceIdsForUid(UID_1)).isEmpty();
@@ -1957,7 +1971,7 @@
                         mRunningAppsChangedCallback,
                         params,
                         new DisplayManagerGlobal(mIDisplayManager),
-                        new VirtualCameraController());
+                        new VirtualCameraController(DEVICE_POLICY_DEFAULT));
         mVdms.addVirtualDevice(virtualDeviceImpl);
         assertThat(virtualDeviceImpl.getAssociationId()).isEqualTo(mAssociationInfo.getId());
         assertThat(virtualDeviceImpl.getPersistentDeviceId())
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
index 9b28b81..3e4f1df 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
@@ -16,27 +16,35 @@
 
 package com.android.server.companion.virtual.camera;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_0;
+import static android.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_90;
+import static android.graphics.ImageFormat.YUV_420_888;
+import static android.graphics.PixelFormat.RGBA_8888;
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_BACK;
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.NonNull;
 import android.companion.virtual.camera.VirtualCameraCallback;
 import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
 import android.companion.virtualcamera.IVirtualCameraService;
 import android.companion.virtualcamera.VirtualCameraConfiguration;
-import android.graphics.ImageFormat;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.platform.test.annotations.Presubmit;
-import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.view.Surface;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
 
 import org.junit.After;
 import org.junit.Before;
@@ -49,21 +57,30 @@
 import java.util.List;
 
 @Presubmit
-@RunWith(AndroidTestingRunner.class)
+@RunWith(JUnitParamsRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class VirtualCameraControllerTest {
 
     private static final String CAMERA_NAME_1 = "Virtual camera 1";
     private static final int CAMERA_WIDTH_1 = 100;
     private static final int CAMERA_HEIGHT_1 = 200;
+    private static final int CAMERA_FORMAT_1 = YUV_420_888;
+    private static final int CAMERA_MAX_FPS_1 = 30;
+    private static final int CAMERA_SENSOR_ORIENTATION_1 = SENSOR_ORIENTATION_0;
+    private static final int CAMERA_LENS_FACING_1 = LENS_FACING_BACK;
 
     private static final String CAMERA_NAME_2 = "Virtual camera 2";
     private static final int CAMERA_WIDTH_2 = 400;
     private static final int CAMERA_HEIGHT_2 = 600;
-    private static final int CAMERA_FORMAT = ImageFormat.YUV_420_888;
+    private static final int CAMERA_FORMAT_2 = RGBA_8888;
+    private static final int CAMERA_MAX_FPS_2 = 60;
+    private static final int CAMERA_SENSOR_ORIENTATION_2 = SENSOR_ORIENTATION_90;
+    private static final int CAMERA_LENS_FACING_2 = LENS_FACING_FRONT;
 
     @Mock
     private IVirtualCameraService mVirtualCameraServiceMock;
+    @Mock
+    private VirtualCameraCallback mVirtualCameraCallbackMock;
 
     private VirtualCameraController mVirtualCameraController;
     private final HandlerExecutor mCallbackHandler =
@@ -72,7 +89,8 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock);
+        mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock,
+                DEVICE_POLICY_CUSTOM);
         when(mVirtualCameraServiceMock.registerCamera(any(), any())).thenReturn(true);
     }
 
@@ -81,10 +99,12 @@
         mVirtualCameraController.close();
     }
 
+    @Parameters(method = "getAllLensFacingDirections")
     @Test
-    public void registerCamera_registersCamera() throws Exception {
+    public void registerCamera_registersCamera(int lensFacing) throws Exception {
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1));
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+                CAMERA_SENSOR_ORIENTATION_1, lensFacing));
 
         ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
                 ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
@@ -92,13 +112,15 @@
         VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue();
         assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1);
         assertVirtualCameraConfiguration(virtualCameraConfiguration, CAMERA_WIDTH_1,
-                CAMERA_HEIGHT_1, CAMERA_FORMAT);
+                CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_SENSOR_ORIENTATION_1,
+                lensFacing);
     }
 
     @Test
     public void unregisterCamera_unregistersCamera() throws Exception {
         VirtualCameraConfig config = createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1);
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+                CAMERA_SENSOR_ORIENTATION_1, CAMERA_LENS_FACING_1);
         mVirtualCameraController.registerCamera(config);
 
         mVirtualCameraController.unregisterCamera(config);
@@ -109,9 +131,11 @@
     @Test
     public void close_unregistersAllCameras() throws Exception {
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1));
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+                CAMERA_SENSOR_ORIENTATION_1, CAMERA_LENS_FACING_1));
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT, CAMERA_NAME_2));
+                CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, CAMERA_NAME_2,
+                CAMERA_SENSOR_ORIENTATION_2, CAMERA_LENS_FACING_2));
 
         mVirtualCameraController.close();
 
@@ -123,38 +147,66 @@
                 configurationCaptor.getAllValues();
         assertThat(virtualCameraConfigurations).hasSize(2);
         assertVirtualCameraConfiguration(virtualCameraConfigurations.get(0), CAMERA_WIDTH_1,
-                CAMERA_HEIGHT_1, CAMERA_FORMAT);
+                CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_SENSOR_ORIENTATION_1,
+                CAMERA_LENS_FACING_1);
         assertVirtualCameraConfiguration(virtualCameraConfigurations.get(1), CAMERA_WIDTH_2,
-                CAMERA_HEIGHT_2, CAMERA_FORMAT);
+                CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, CAMERA_SENSOR_ORIENTATION_2,
+                CAMERA_LENS_FACING_2);
+    }
+
+    @Parameters(method = "getAllLensFacingDirections")
+    @Test
+    public void registerMultipleSameLensFacingCameras_withCustomCameraPolicy_throwsException(
+            int lensFacing) {
+        mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+                CAMERA_SENSOR_ORIENTATION_1, lensFacing));
+        assertThrows(IllegalArgumentException.class,
+                () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                        CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2,
+                        CAMERA_NAME_2, CAMERA_SENSOR_ORIENTATION_2, lensFacing)));
+    }
+
+    @Parameters(method = "getAllLensFacingDirections")
+    @Test
+    public void registerCamera_withDefaultCameraPolicy_throwsException(int lensFacing) {
+        mVirtualCameraController.close();
+        mVirtualCameraController = new VirtualCameraController(
+                mVirtualCameraServiceMock, DEVICE_POLICY_DEFAULT);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                        CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1,
+                        CAMERA_NAME_1, CAMERA_SENSOR_ORIENTATION_1, lensFacing)));
     }
 
     private VirtualCameraConfig createVirtualCameraConfig(
-            int width, int height, int format, String displayName) {
+            int width, int height, int format, int maximumFramesPerSecond,
+            String name, int sensorOrientation, int lensFacing) {
         return new VirtualCameraConfig.Builder()
-                .addStreamConfig(width, height, format)
-                .setName(displayName)
-                .setVirtualCameraCallback(mCallbackHandler, createNoOpCallback())
+                .addStreamConfig(width, height, format, maximumFramesPerSecond)
+                .setName(name)
+                .setVirtualCameraCallback(mCallbackHandler, mVirtualCameraCallbackMock)
+                .setSensorOrientation(sensorOrientation)
+                .setLensFacing(lensFacing)
                 .build();
     }
 
     private static void assertVirtualCameraConfiguration(
-            VirtualCameraConfiguration configuration, int width, int height, int format) {
+            VirtualCameraConfiguration configuration, int width, int height, int format,
+            int maxFps, int sensorOrientation, int lensFacing) {
         assertThat(configuration.supportedStreamConfigs[0].width).isEqualTo(width);
         assertThat(configuration.supportedStreamConfigs[0].height).isEqualTo(height);
         assertThat(configuration.supportedStreamConfigs[0].pixelFormat).isEqualTo(format);
+        assertThat(configuration.supportedStreamConfigs[0].maxFps).isEqualTo(maxFps);
+        assertThat(configuration.sensorOrientation).isEqualTo(sensorOrientation);
+        assertThat(configuration.lensFacing).isEqualTo(lensFacing);
     }
 
-    private static VirtualCameraCallback createNoOpCallback() {
-        return new VirtualCameraCallback() {
-
-            @Override
-            public void onStreamConfigured(
-                    int streamId,
-                    @NonNull Surface surface,
-                    @NonNull VirtualCameraStreamConfig streamConfig) {}
-
-            @Override
-            public void onStreamClosed(int streamId) {}
+    private static Integer[] getAllLensFacingDirections() {
+        return new Integer[] {
+                LENS_FACING_BACK,
+                LENS_FACING_FRONT
         };
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
index d9a38eb..206c111 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
@@ -35,19 +35,20 @@
 
     private static final int VGA_WIDTH = 640;
     private static final int VGA_HEIGHT = 480;
+    private static final int MAX_FPS_1 = 30;
 
     private static final int QVGA_WIDTH = 320;
     private static final int QVGA_HEIGHT = 240;
+    private static final int MAX_FPS_2 = 60;
 
     @Test
     public void testEquals() {
         VirtualCameraStreamConfig vgaYuvStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
-                VGA_HEIGHT,
-                ImageFormat.YUV_420_888);
+                VGA_HEIGHT, ImageFormat.YUV_420_888, MAX_FPS_1);
         VirtualCameraStreamConfig qvgaYuvStreamConfig = new VirtualCameraStreamConfig(QVGA_WIDTH,
-                QVGA_HEIGHT, ImageFormat.YUV_420_888);
+                QVGA_HEIGHT, ImageFormat.YUV_420_888, MAX_FPS_2);
         VirtualCameraStreamConfig vgaRgbaStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
-                VGA_HEIGHT, PixelFormat.RGBA_8888);
+                VGA_HEIGHT, PixelFormat.RGBA_8888, MAX_FPS_1);
 
         new EqualsTester()
                 .addEqualityGroup(vgaYuvStreamConfig, reparcel(vgaYuvStreamConfig))
@@ -66,6 +67,4 @@
             parcel.recycle();
         }
     }
-
-
 }
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java
new file mode 100644
index 0000000..a55d1c4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.inputmethod;
+
+import static org.junit.Assert.assertEquals;
+
+import android.text.TextUtils;
+import android.util.IntArray;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class InputMethodSettingsTest {
+    private static void verifyUpdateEnabledImeString(@NonNull String expectedEnabledImeStr,
+            @NonNull String initialEnabledImeStr, @NonNull String imeId,
+            @NonNull String enabledSubtypeHashCodesStr) {
+        assertEquals(expectedEnabledImeStr,
+                InputMethodSettings.updateEnabledImeString(initialEnabledImeStr,
+                        imeId, createSubtypeHashCodeArrayFromStr(enabledSubtypeHashCodesStr)));
+    }
+
+    private static IntArray createSubtypeHashCodeArrayFromStr(String subtypeHashCodesStr) {
+        final IntArray subtypes = new IntArray();
+        final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
+                new TextUtils.SimpleStringSplitter(';');
+        if (TextUtils.isEmpty(subtypeHashCodesStr)) {
+            return subtypes;
+        }
+        imeSubtypeSplitter.setString(subtypeHashCodesStr);
+        while (imeSubtypeSplitter.hasNext()) {
+            subtypes.add(Integer.parseInt(imeSubtypeSplitter.next()));
+        }
+        return subtypes;
+    }
+
+    @Test
+    public void updateEnabledImeStringTest() {
+        // No change cases
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1",
+                "com.android/.ime1", "com.android/.ime1", "");
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1",
+                "com.android/.ime1", "com.android/.ime2", "");
+
+        // To enable subtypes
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1",
+                "com.android/.ime1", "com.android/.ime2", "");
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1;1",
+                "com.android/.ime1", "com.android/.ime1", "1");
+
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1;1;2;3",
+                "com.android/.ime1", "com.android/.ime1", "1;2;3");
+
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1;1;2;3:com.android/.ime2",
+                "com.android/.ime1:com.android/.ime2", "com.android/.ime1", "1;2;3");
+        verifyUpdateEnabledImeString(
+                "com.android/.ime0:com.android/.ime1;1;2;3",
+                "com.android/.ime0:com.android/.ime1", "com.android/.ime1", "1;2;3");
+        verifyUpdateEnabledImeString(
+                "com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2",
+                "com.android/.ime0:com.android/.ime1:com.android/.ime2", "com.android/.ime1",
+                "1;2;3");
+
+        // To reset enabled subtypes
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1",
+                "com.android/.ime1;1", "com.android/.ime1", "");
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1",
+                "com.android/.ime1;1;2;3", "com.android/.ime1", "");
+        verifyUpdateEnabledImeString(
+                "com.android/.ime1:com.android/.ime2",
+                "com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1", "");
+
+        verifyUpdateEnabledImeString(
+                "com.android/.ime0:com.android/.ime1",
+                "com.android/.ime0:com.android/.ime1;1;2;3", "com.android/.ime1", "");
+        verifyUpdateEnabledImeString(
+                "com.android/.ime0:com.android/.ime1:com.android/.ime2",
+                "com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1",
+                "");
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
index 6b85a32..9688ef6 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
@@ -25,28 +25,16 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
-import android.content.ContentResolver;
 import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.IContentProvider;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.os.Build;
 import android.os.LocaleList;
 import android.os.Parcel;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.test.mock.MockContentResolver;
-import android.text.TextUtils;
 import android.util.ArrayMap;
-import android.util.IntArray;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
@@ -1221,85 +1209,6 @@
                 StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR));
     }
 
-    private static IntArray createSubtypeHashCodeArrayFromStr(String subtypeHashCodesStr) {
-        final IntArray subtypes = new IntArray();
-        final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
-                new TextUtils.SimpleStringSplitter(';');
-        if (TextUtils.isEmpty(subtypeHashCodesStr)) {
-            return subtypes;
-        }
-        imeSubtypeSplitter.setString(subtypeHashCodesStr);
-        while (imeSubtypeSplitter.hasNext()) {
-            subtypes.add(Integer.parseInt(imeSubtypeSplitter.next()));
-        }
-        return subtypes;
-    }
-
-    private static void verifyUpdateEnabledImeString(@NonNull String expectedEnabledImeStr,
-            @NonNull String initialEnabledImeStr, @NonNull String imeId,
-            @NonNull String enabledSubtypeHashCodesStr) {
-        assertEquals(expectedEnabledImeStr,
-                InputMethodUtils.InputMethodSettings.updateEnabledImeString(initialEnabledImeStr,
-                        imeId, createSubtypeHashCodeArrayFromStr(enabledSubtypeHashCodesStr)));
-    }
-
-    private static TestContext createMockContext(int userId) {
-        return new TestContext(InstrumentationRegistry.getInstrumentation()
-                .getTargetContext(), userId);
-    }
-
-    private static class TestContext extends ContextWrapper {
-        private int mUserId;
-        private ContentResolver mResolver;
-        private Resources mResources;
-
-        private static TestContext sSecondaryUserContext;
-
-        TestContext(@NonNull Context context, int userId) {
-            super(context);
-            mUserId = userId;
-            mResolver = mock(MockContentResolver.class);
-            when(mResolver.acquireProvider(Settings.Secure.CONTENT_URI)).thenReturn(
-                    mock(IContentProvider.class));
-            mResources = mock(Resources.class);
-
-            final Configuration configuration = new Configuration();
-            if (userId == 0) {
-                configuration.setLocale(LOCALE_EN_US);
-            } else {
-                configuration.setLocale(LOCALE_FR_CA);
-            }
-            doReturn(configuration).when(mResources).getConfiguration();
-        }
-
-        @Override
-        public Context createContextAsUser(UserHandle user, int flags) {
-            if (user.getIdentifier() != UserHandle.USER_SYSTEM) {
-                return sSecondaryUserContext = new TestContext(this, user.getIdentifier());
-            }
-            return this;
-        }
-
-        @Override
-        public int getUserId() {
-            return mUserId;
-        }
-
-        @Override
-        public ContentResolver getContentResolver() {
-            return mResolver;
-        }
-
-        @Override
-        public Resources getResources() {
-            return mResources;
-        }
-
-        static Context getSecondaryUserContext() {
-            return sSecondaryUserContext;
-        }
-    }
-
     private static void verifySplitEnabledImeStr(@NonNull String enabledImeStr,
             @NonNull String... expected) {
         final ArrayList<String> actual = new ArrayList<>();
@@ -1347,57 +1256,4 @@
                         "com.android/.ime1:com.android/.ime2", "com.android/.ime3"))
                 .isEqualTo("com.android/.ime1:com.android/.ime2:com.android/.ime3");
     }
-
-    @Test
-    public void updateEnabledImeStringTest() {
-        // No change cases
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1",
-                "com.android/.ime1", "com.android/.ime1", "");
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1",
-                "com.android/.ime1", "com.android/.ime2", "");
-
-        // To enable subtypes
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1",
-                "com.android/.ime1", "com.android/.ime2", "");
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1;1",
-                "com.android/.ime1", "com.android/.ime1", "1");
-
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1;1;2;3",
-                "com.android/.ime1", "com.android/.ime1", "1;2;3");
-
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1;1;2;3:com.android/.ime2",
-                "com.android/.ime1:com.android/.ime2", "com.android/.ime1", "1;2;3");
-        verifyUpdateEnabledImeString(
-                "com.android/.ime0:com.android/.ime1;1;2;3",
-                "com.android/.ime0:com.android/.ime1", "com.android/.ime1", "1;2;3");
-        verifyUpdateEnabledImeString(
-                "com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2",
-                "com.android/.ime0:com.android/.ime1:com.android/.ime2", "com.android/.ime1",
-                "1;2;3");
-
-        // To reset enabled subtypes
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1",
-                "com.android/.ime1;1", "com.android/.ime1", "");
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1",
-                "com.android/.ime1;1;2;3", "com.android/.ime1", "");
-        verifyUpdateEnabledImeString(
-                "com.android/.ime1:com.android/.ime2",
-                "com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1", "");
-
-        verifyUpdateEnabledImeString(
-                "com.android/.ime0:com.android/.ime1",
-                "com.android/.ime0:com.android/.ime1;1;2;3", "com.android/.ime1", "");
-        verifyUpdateEnabledImeString(
-                "com.android/.ime0:com.android/.ime1:com.android/.ime2",
-                "com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1",
-                "");
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
index 769ec5f..3218586 100644
--- a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
@@ -345,15 +345,18 @@
                 intent, UID_PRIMARY_CAMERA, PKG_SOCIAL, USER_PRIMARY), service);
 
         // Verify that everything is good with the world
-        assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+        assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+                /* isFullAccessForContentUri */ false));
 
         // Finish activity; service should hold permission
         activity.removeUriPermissions();
-        assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+        assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+                /* isFullAccessForContentUri */ false));
 
         // And finishing service should wrap things up
         service.removeUriPermissions();
-        assertFalse(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+        assertFalse(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+                /* isFullAccessForContentUri */ false));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
index 3530e38..ae0a758 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
@@ -85,7 +85,7 @@
     private void enablePackageForUser(String packageName, boolean enable, int userId) {
         Map<Integer, PackageInfo> userPackages = mPackages.get(packageName);
         if (userPackages == null) {
-            throw new IllegalArgumentException("There is no package called " + packageName);
+            return;
         }
         PackageInfo packageInfo = userPackages.get(userId);
         packageInfo.applicationInfo.enabled = enable;
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index 32082e3..5a06327 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -127,12 +127,21 @@
     private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
             WebViewProviderInfo[] webviewPackages) {
         checkCertainPackageUsedAfterWebViewBootPreparation(
-                expectedProviderName, webviewPackages, 1);
+                expectedProviderName, webviewPackages, 1, null);
     }
 
     private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
-            WebViewProviderInfo[] webviewPackages, int numRelros) {
+            WebViewProviderInfo[] webviewPackages, String userSetting) {
+        checkCertainPackageUsedAfterWebViewBootPreparation(
+                expectedProviderName, webviewPackages, 1, userSetting);
+    }
+
+    private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
+            WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) {
         setupWithPackagesAndRelroCount(webviewPackages, numRelros);
+        if (userSetting != null) {
+            mTestSystemImpl.updateUserSetting(null, userSetting);
+        }
         // Add (enabled and valid) package infos for each provider
         setEnabledAndValidPackageInfos(webviewPackages);
 
@@ -280,7 +289,7 @@
                 singlePackage,
                 new WebViewProviderInfo[] {
                     new WebViewProviderInfo(singlePackage, "", true /*def av*/, false, null)},
-                2);
+                2, null);
     }
 
     // Ensure that package with valid signatures is chosen rather than package with invalid
@@ -295,14 +304,16 @@
         Signature invalidPackageSignature = new Signature("33");
 
         WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(invalidPackage, "", true, false, new String[]{
-                        Base64.encodeToString(
-                                invalidExpectedSignature.toByteArray(), Base64.DEFAULT)}),
             new WebViewProviderInfo(validPackage, "", true, false, new String[]{
                         Base64.encodeToString(
-                                validSignature.toByteArray(), Base64.DEFAULT)})
+                                validSignature.toByteArray(), Base64.DEFAULT)}),
+            new WebViewProviderInfo(invalidPackage, "", true, false, new String[]{
+                        Base64.encodeToString(
+                                invalidExpectedSignature.toByteArray(), Base64.DEFAULT)})
         };
         setupWithPackagesNonDebuggable(packages);
+        // Start with the setting pointing to the invalid package
+        mTestSystemImpl.updateUserSetting(null, invalidPackage);
         mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */,
                     true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature}
                     , 0 /* updateTime */));
@@ -339,7 +350,9 @@
     }
 
     @Test
-    public void testFailListingEmptyWebviewPackages() {
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
+    // If the flag is set, will throw an exception because of no available by default provider.
+    public void testEmptyConfig() {
         WebViewProviderInfo[] packages = new WebViewProviderInfo[0];
         setupWithPackages(packages);
         setEnabledAndValidPackageInfos(packages);
@@ -352,14 +365,26 @@
         WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
         assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status);
         assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage());
+    }
 
-        // Now install a package
+    @Test
+    public void testFailListingEmptyWebviewPackages() {
         String singlePackage = "singlePackage";
-        packages = new WebViewProviderInfo[]{
+        WebViewProviderInfo[] packages = new WebViewProviderInfo[]{
             new WebViewProviderInfo(singlePackage, "", true, false, null)};
         setupWithPackages(packages);
-        setEnabledAndValidPackageInfos(packages);
 
+        runWebViewBootPreparationOnMainSync();
+
+        Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged(
+                Matchers.anyObject());
+
+        WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
+        assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status);
+        assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage());
+
+        // Now install the package
+        setEnabledAndValidPackageInfos(packages);
         mWebViewUpdateServiceImpl.packageStateChanged(singlePackage,
                 WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID);
 
@@ -370,7 +395,7 @@
         // Remove the package again
         mTestSystemImpl.removePackageInfo(singlePackage);
         mWebViewUpdateServiceImpl.packageStateChanged(singlePackage,
-                WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID);
+                WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID);
 
         // Package removed - ensure our interface states that there is no package
         response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
@@ -455,6 +480,8 @@
             new WebViewProviderInfo(firstPackage, "", true, false, null),
             new WebViewProviderInfo(secondPackage, "", true, false, null)};
         setupWithPackages(packages);
+        // Start with the setting pointing to the second package
+        mTestSystemImpl.updateUserSetting(null, secondPackage);
         // Have all packages be enabled, so that we can change provider however we want to
         setEnabledAndValidPackageInfos(packages);
 
@@ -463,9 +490,9 @@
         runWebViewBootPreparationOnMainSync();
 
         Mockito.verify(mTestSystemImpl).onWebViewProviderChanged(
-                Mockito.argThat(new IsPackageInfoWithName(firstPackage)));
+                Mockito.argThat(new IsPackageInfoWithName(secondPackage)));
 
-        assertEquals(firstPackage,
+        assertEquals(secondPackage,
                 mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
 
         new Thread(new Runnable() {
@@ -474,12 +501,13 @@
                 WebViewProviderResponse threadResponse =
                     mWebViewUpdateServiceImpl.waitForAndGetProvider();
                 assertEquals(WebViewFactory.LIBLOAD_SUCCESS, threadResponse.status);
-                assertEquals(secondPackage, threadResponse.packageInfo.packageName);
-                // Verify that we killed the first package if we performed a settings change -
-                // otherwise we had to disable the first package, in which case its dependents
+                assertEquals(firstPackage, threadResponse.packageInfo.packageName);
+                // Verify that we killed the second package if we performed a settings change -
+                // otherwise we had to disable the second package, in which case its dependents
                 // should have been killed by the framework.
                 if (settingsChange) {
-                    Mockito.verify(mTestSystemImpl).killPackageDependents(Mockito.eq(firstPackage));
+                    Mockito.verify(mTestSystemImpl)
+                            .killPackageDependents(Mockito.eq(secondPackage));
                 }
                 countdown.countDown();
             }
@@ -490,32 +518,36 @@
         }
 
         if (settingsChange) {
-            mWebViewUpdateServiceImpl.changeProviderAndSetting(secondPackage);
+            mWebViewUpdateServiceImpl.changeProviderAndSetting(firstPackage);
         } else {
-            // Enable the second provider
-            mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, true /* enabled */,
+            // Enable the first provider
+            mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, true /* enabled */,
                         true /* valid */, true /* installed */));
             mWebViewUpdateServiceImpl.packageStateChanged(
-                    secondPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID);
+                    firstPackage,
+                    WebViewUpdateService.PACKAGE_CHANGED,
+                    TestSystemImpl.PRIMARY_USER_ID);
 
             // Ensure we haven't changed package yet.
-            assertEquals(firstPackage,
+            assertEquals(secondPackage,
                     mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
 
-            // Switch provider by disabling the first one
-            mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, false /* enabled */,
+            // Switch provider by disabling the second one
+            mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, false /* enabled */,
                         true /* valid */, true /* installed */));
             mWebViewUpdateServiceImpl.packageStateChanged(
-                    firstPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID);
+                    secondPackage,
+                    WebViewUpdateService.PACKAGE_CHANGED,
+                    TestSystemImpl.PRIMARY_USER_ID);
         }
         mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
-        // first package done, should start on second
+        // second package done, should start on first
 
         Mockito.verify(mTestSystemImpl).onWebViewProviderChanged(
-                Mockito.argThat(new IsPackageInfoWithName(secondPackage)));
+                Mockito.argThat(new IsPackageInfoWithName(firstPackage)));
 
         mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
-        // second package done, the other thread should now be unblocked
+        // first package done, the other thread should now be unblocked
         try {
             countdown.await();
         } catch (InterruptedException e) {
@@ -526,6 +558,7 @@
      * Scenario for testing re-enabling a fallback package.
      */
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
     public void testFallbackPackageEnabling() {
         String testPackage = "testFallback";
         WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
@@ -555,6 +588,9 @@
      * 3. Primary should be used
      */
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
+    // If the flag is set, we don't automitally switch to secondary package unless it is
+    // chosen directly.
     public void testInstallingPrimaryPackage() {
         String primaryPackage = "primary";
         String secondaryPackage = "secondary";
@@ -586,16 +622,16 @@
     }
 
     @Test
-    public void testRemovingPrimarySelectsSecondarySingleUser() {
+    public void testRemovingSecondarySelectsPrimarySingleUser() {
         for (PackageRemovalType removalType : REMOVAL_TYPES) {
-            checkRemovingPrimarySelectsSecondary(false /* multiUser */, removalType);
+            checkRemovingSecondarySelectsPrimary(false /* multiUser */, removalType);
         }
     }
 
     @Test
-    public void testRemovingPrimarySelectsSecondaryMultiUser() {
+    public void testRemovingSecondarySelectsPrimaryMultiUser() {
         for (PackageRemovalType removalType : REMOVAL_TYPES) {
-            checkRemovingPrimarySelectsSecondary(true /* multiUser */, removalType);
+            checkRemovingSecondarySelectsPrimary(true /* multiUser */, removalType);
         }
     }
 
@@ -609,7 +645,7 @@
 
     private PackageRemovalType[] REMOVAL_TYPES = PackageRemovalType.class.getEnumConstants();
 
-    public void checkRemovingPrimarySelectsSecondary(boolean multiUser,
+    private void checkRemovingSecondarySelectsPrimary(boolean multiUser,
             PackageRemovalType removalType) {
         String primaryPackage = "primary";
         String secondaryPackage = "secondary";
@@ -620,6 +656,8 @@
                     secondaryPackage, "", true /* default available */, false /* fallback */,
                     null)};
         setupWithPackages(packages);
+        // Start with the setting pointing to the secondary package
+        mTestSystemImpl.updateUserSetting(null, secondaryPackage);
         int secondaryUserId = 10;
         int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID;
         if (multiUser) {
@@ -629,31 +667,31 @@
         setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages);
 
         runWebViewBootPreparationOnMainSync();
-        checkPreparationPhasesForPackage(primaryPackage, 1);
+        checkPreparationPhasesForPackage(secondaryPackage, 1);
 
         boolean enabled = !(removalType == PackageRemovalType.DISABLE);
         boolean installed = !(removalType == PackageRemovalType.UNINSTALL);
         boolean hidden = (removalType == PackageRemovalType.HIDE);
-        // Disable primary package and ensure secondary becomes used
+        // Disable secondary package and ensure primary becomes used
         mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor,
-                createPackageInfo(primaryPackage, enabled /* enabled */, true /* valid */,
+                createPackageInfo(secondaryPackage, enabled /* enabled */, true /* valid */,
                     installed /* installed */, null /* signature */, 0 /* updateTime */,
                     hidden /* hidden */));
-        mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage,
+        mWebViewUpdateServiceImpl.packageStateChanged(secondaryPackage,
                 removalType == PackageRemovalType.DISABLE
                 ? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_REMOVED,
                 userIdToChangePackageFor); // USER ID
-        checkPreparationPhasesForPackage(secondaryPackage, 1);
+        checkPreparationPhasesForPackage(primaryPackage, 1);
 
-        // Again enable primary package and verify primary is used
+        // Again enable secondary package and verify secondary is used
         mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor,
-                createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
+                createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */,
                     true /* installed */));
-        mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage,
+        mWebViewUpdateServiceImpl.packageStateChanged(secondaryPackage,
                 removalType == PackageRemovalType.DISABLE
                 ? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_ADDED,
                 userIdToChangePackageFor);
-        checkPreparationPhasesForPackage(primaryPackage, 2);
+        checkPreparationPhasesForPackage(secondaryPackage, 2);
     }
 
     /**
@@ -671,18 +709,20 @@
                     secondaryPackage, "", true /* default available */, false /* fallback */,
                     null)};
         setupWithPackages(packages);
+        // Start with the setting pointing to the secondary package
+        mTestSystemImpl.updateUserSetting(null, secondaryPackage);
         setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages);
         int newUser = 100;
         mTestSystemImpl.addUser(newUser);
-        // Let the primary package be uninstalled for the new user
-        mTestSystemImpl.setPackageInfoForUser(newUser,
-                createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
-                        false /* installed */));
+        // Let the secondary package be uninstalled for the new user
         mTestSystemImpl.setPackageInfoForUser(newUser,
                 createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */,
+                        false /* installed */));
+        mTestSystemImpl.setPackageInfoForUser(newUser,
+                createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
                         true /* installed */));
         mWebViewUpdateServiceImpl.handleNewUser(newUser);
-        checkPreparationPhasesForPackage(secondaryPackage, 1 /* numRelros */);
+        checkPreparationPhasesForPackage(primaryPackage, 1 /* numRelros */);
     }
 
     /**
@@ -780,9 +820,9 @@
         String chosenPackage = "chosenPackage";
         String nonChosenPackage = "non-chosenPackage";
         WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(chosenPackage, "", true /* default available */,
-                    false /* fallback */, null),
             new WebViewProviderInfo(nonChosenPackage, "", true /* default available */,
+                    false /* fallback */, null),
+            new WebViewProviderInfo(chosenPackage, "", true /* default available */,
                     false /* fallback */, null)};
 
         setupWithPackages(packages);
@@ -810,6 +850,9 @@
     }
 
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
+    // If the flag is set, we don't automitally switch to second package unless it is chosen
+    // directly.
     public void testRecoverFailedListingWebViewPackagesAddedPackage() {
         checkRecoverAfterFailListingWebviewPackages(false);
     }
@@ -874,22 +917,22 @@
                     false /* fallback */, null),
             new WebViewProviderInfo(secondPackage, "", true /* default available */,
                     false /* fallback */, null)};
-        checkCertainPackageUsedAfterWebViewBootPreparation(firstPackage, packages);
+        checkCertainPackageUsedAfterWebViewBootPreparation(secondPackage, packages, secondPackage);
 
         // Replace or remove the current webview package
         if (replaced) {
             mTestSystemImpl.setPackageInfo(
-                    createPackageInfo(firstPackage, true /* enabled */, false /* valid */,
+                    createPackageInfo(secondPackage, true /* enabled */, false /* valid */,
                         true /* installed */));
-            mWebViewUpdateServiceImpl.packageStateChanged(firstPackage,
+            mWebViewUpdateServiceImpl.packageStateChanged(secondPackage,
                     WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID);
         } else {
-            mTestSystemImpl.removePackageInfo(firstPackage);
-            mWebViewUpdateServiceImpl.packageStateChanged(firstPackage,
+            mTestSystemImpl.removePackageInfo(secondPackage);
+            mWebViewUpdateServiceImpl.packageStateChanged(secondPackage,
                     WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID);
         }
 
-        checkPreparationPhasesForPackage(secondPackage, 1);
+        checkPreparationPhasesForPackage(firstPackage, 1);
 
         Mockito.verify(mTestSystemImpl, Mockito.never()).killPackageDependents(
                 Mockito.anyObject());
@@ -1073,10 +1116,12 @@
     }
 
     /**
-     * Ensure that the update service does use an uninstalled package when that is the only
+     * Ensure that the update service does not use an uninstalled package even if it is the only
      * package available.
      */
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
+    // If the flag is set, we return the package even if it is not installed.
     public void testWithSingleUninstalledPackage() {
         String testPackageName = "test.package.name";
         WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
@@ -1115,12 +1160,14 @@
         String installedPackage = "installedPackage";
         String uninstalledPackage = "uninstalledPackage";
         WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
-                    false /* fallback */, null),
             new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+                    false /* fallback */, null),
+            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
                     false /* fallback */, null)};
 
         setupWithPackages(webviewPackages);
+        // Start with the setting pointing to the uninstalled package
+        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
         int secondaryUserId = 5;
         if (multiUser) {
             mTestSystemImpl.addUser(secondaryUserId);
@@ -1128,7 +1175,7 @@
             setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, webviewPackages);
             mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo(
                     installedPackage, true /* enabled */, true /* valid */, true /* installed */));
-            // Hide or uninstall the primary package for the second user
+            // Hide or uninstall the secondary package for the second user
             mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */,
                     true /* valid */, (testUninstalled ? false : true) /* installed */,
                     null /* signatures */, 0 /* updateTime */, (testHidden ? true : false)));
@@ -1166,12 +1213,14 @@
         String installedPackage = "installedPackage";
         String uninstalledPackage = "uninstalledPackage";
         WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
-                    false /* fallback */, null),
             new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+                    false /* fallback */, null),
+            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
                     false /* fallback */, null)};
 
         setupWithPackages(webviewPackages);
+        // Start with the setting pointing to the uninstalled package
+        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
         int secondaryUserId = 412;
         mTestSystemImpl.addUser(secondaryUserId);
 
@@ -1221,12 +1270,14 @@
         String installedPackage = "installedPackage";
         String uninstalledPackage = "uninstalledPackage";
         WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
-                    false /* fallback */, null),
             new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+                    false /* fallback */, null),
+            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
                     false /* fallback */, null)};
 
         setupWithPackages(webviewPackages);
+        // Start with the setting pointing to the uninstalled package
+        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
         int secondaryUserId = 4;
         mTestSystemImpl.addUser(secondaryUserId);
 
@@ -1433,11 +1484,16 @@
                 new WebViewProviderInfo(newSdkPackage.packageName, "", true, false, null);
         WebViewProviderInfo currentSdkProviderInfo =
                 new WebViewProviderInfo(currentSdkPackage.packageName, "", true, false, null);
-        WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(oldSdkPackage.packageName, "", true, false, null),
-            currentSdkProviderInfo, newSdkProviderInfo};
+        WebViewProviderInfo[] packages =
+                new WebViewProviderInfo[] {
+                    currentSdkProviderInfo,
+                    new WebViewProviderInfo(oldSdkPackage.packageName, "", true, false, null),
+                    newSdkProviderInfo
+                };
         setupWithPackages(packages);
-;
+        // Start with the setting pointing to the invalid package
+        mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName);
+
         mTestSystemImpl.setPackageInfo(newSdkPackage);
         mTestSystemImpl.setPackageInfo(currentSdkPackage);
         mTestSystemImpl.setPackageInfo(oldSdkPackage);
@@ -1467,4 +1523,74 @@
         assertEquals(
                 defaultPackage1, mWebViewUpdateServiceImpl.getDefaultWebViewPackage().packageName);
     }
+
+    @Test
+    @RequiresFlagsEnabled("android.webkit.update_service_v2")
+    public void testDefaultWebViewPackageEnabling() {
+        String testPackage = "testDefault";
+        WebViewProviderInfo[] packages =
+                new WebViewProviderInfo[] {
+                    new WebViewProviderInfo(
+                            testPackage,
+                            "",
+                            true /* default available */,
+                            false /* fallback */,
+                            null)
+                };
+        setupWithPackages(packages);
+        mTestSystemImpl.setPackageInfo(
+                createPackageInfo(
+                        testPackage, false /* enabled */, true /* valid */, true /* installed */));
+
+        // Check that the boot time logic re-enables the default package.
+        runWebViewBootPreparationOnMainSync();
+        Mockito.verify(mTestSystemImpl)
+                .enablePackageForAllUsers(
+                        Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true));
+    }
+
+    private void testDefaultPackageChosen(PackageInfo packageInfo) {
+        WebViewProviderInfo[] packages =
+                new WebViewProviderInfo[] {
+                    new WebViewProviderInfo(packageInfo.packageName, "", true, false, null)
+                };
+        setupWithPackages(packages);
+        mTestSystemImpl.setPackageInfo(packageInfo);
+
+        runWebViewBootPreparationOnMainSync();
+        mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
+
+        assertEquals(
+                packageInfo.packageName,
+                mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
+
+        WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
+        assertEquals(packageInfo.packageName, response.packageInfo.packageName);
+    }
+
+    @Test
+    @RequiresFlagsEnabled("android.webkit.update_service_v2")
+    public void testDisabledDefaultPackageChosen() {
+        PackageInfo disabledPackage =
+                createPackageInfo(
+                        "disabledPackage",
+                        false /* enabled */,
+                        true /* valid */,
+                        true /* installed */);
+
+        testDefaultPackageChosen(disabledPackage);
+    }
+
+    @Test
+    @RequiresFlagsEnabled("android.webkit.update_service_v2")
+    public void testUninstalledDefaultPackageChosen() {
+        PackageInfo uninstalledPackage =
+                createPackageInfo(
+                        "uninstalledPackage",
+                        true /* enabled */,
+                        true /* valid */,
+                        false /* installed */);
+
+        testDefaultPackageChosen(uninstalledPackage);
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
index 08af09c..0e20daf 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -143,12 +143,12 @@
                 Policy.policyState(false, true), 0);
 
         ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
-        assertThat(zenPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
 
         Policy notAllowed = new Policy(0, 0, 0, 0,
                 Policy.policyState(false, false), 0);
         ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed);
-        assertThat(zenPolicyNotAllowed.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+        assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
     }
 
     @Test
@@ -158,12 +158,11 @@
                 Policy.policyState(false, true), 0);
 
         ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
-        assertThat(zenPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET);
+        assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
 
         Policy notAllowed = new Policy(0, 0, 0, 0,
                 Policy.policyState(false, false), 0);
         ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed);
-        assertThat(zenPolicyNotAllowed.getAllowedChannels())
-                .isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET);
+        assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
     }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index dd252f3..db92719 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -164,7 +164,7 @@
                 .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
                 .showLights(false)
                 .showInAmbientDisplay(false)
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+                .allowPriorityChannels(false)
                 .build();
 
         Policy originalPolicy = config.toNotificationPolicy();
@@ -255,7 +255,7 @@
                 .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
                 .allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED)
                 .allowConversations(ZenPolicy.CONVERSATION_SENDERS_NONE)
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+                .allowPriorityChannels(false)
                 .build();
 
         ZenModeConfig config = getMutedAllConfig();
@@ -284,7 +284,7 @@
                 actual.getPriorityConversationSenders());
         assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders());
         assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders());
-        assertEquals(expected.getAllowedChannels(), actual.getAllowedChannels());
+        assertEquals(expected.getPriorityChannels(), actual.getPriorityChannels());
         assertEquals(expected.getUserModifiedFields(), actual.getUserModifiedFields());
     }
 
@@ -694,7 +694,7 @@
                 .allowSystem(true)
                 .allowReminders(false)
                 .allowEvents(true)
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+                .allowPriorityChannels(false)
                 .hideAllVisualEffects()
                 .showVisualEffect(ZenPolicy.VISUAL_EFFECT_AMBIENT, true)
                 .setUserModifiedFields(4)
@@ -721,7 +721,7 @@
         assertEquals(policy.getPriorityCategorySystem(), fromXml.getPriorityCategorySystem());
         assertEquals(policy.getPriorityCategoryReminders(), fromXml.getPriorityCategoryReminders());
         assertEquals(policy.getPriorityCategoryEvents(), fromXml.getPriorityCategoryEvents());
-        assertEquals(policy.getAllowedChannels(), fromXml.getAllowedChannels());
+        assertEquals(policy.getPriorityChannels(), fromXml.getPriorityChannels());
 
         assertEquals(policy.getVisualEffectFullScreenIntent(),
                 fromXml.getVisualEffectFullScreenIntent());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
index 29208f4..7d6e12c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
@@ -546,13 +546,13 @@
         // Create a policy to allow channels through, which means shouldIntercept is false
         ZenModeConfig config = new ZenModeConfig();
         Policy policy = config.toNotificationPolicy(new ZenPolicy.Builder()
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .allowPriorityChannels(true)
                 .build());
         assertFalse(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
 
         // Now create a policy which does not allow priority channels:
         policy = config.toNotificationPolicy(new ZenPolicy.Builder()
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+                .allowPriorityChannels(false)
                 .build());
         assertTrue(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 9e3e336..78fe41f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -1151,7 +1151,7 @@
                 .allowAlarms(true)
                 .allowRepeatCallers(false)
                 .allowCalls(PEOPLE_TYPE_STARRED)
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+                .allowPriorityChannels(false)
                 .build();
         mZenModeHelper.mConfig.automaticRules.put(rule.id, rule);
         List<StatsEvent> events = new LinkedList<>();
@@ -1174,7 +1174,7 @@
                 assertThat(policy.getAllowCallsFrom().getNumber())
                         .isEqualTo(DNDProtoEnums.PEOPLE_STARRED);
                 assertThat(policy.getAllowChannels().getNumber())
-                        .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE);
+                        .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_NONE);
             }
         }
         assertTrue("couldn't find custom rule", foundCustomEvent);
@@ -3098,7 +3098,7 @@
         DNDPolicyProto origDndProto = mZenModeEventLogger.getPolicyProto(0);
         checkDndProtoMatchesSetupZenConfig(origDndProto);
         assertThat(origDndProto.getAllowChannels().getNumber())
-                .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_PRIORITY);
+                .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_PRIORITY);
 
         // Second message where we change the policy:
         //   - DND_POLICY_CHANGED (indicates only the policy changed and nothing else)
@@ -3110,7 +3110,7 @@
                 .isEqualTo(DNDProtoEnums.UNKNOWN_RULE);
         DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(1);
         assertThat(dndProto.getAllowChannels().getNumber())
-                .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE);
+                .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_NONE);
     }
 
     @Test
@@ -3299,7 +3299,7 @@
 
         // one rule, custom policy, allows channels
         ZenPolicy customPolicy = new ZenPolicy.Builder()
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .allowPriorityChannels(true)
                 .build();
 
         AutomaticZenRule zenRule = new AutomaticZenRule("name",
@@ -3321,7 +3321,7 @@
 
         // add new rule with policy that disallows channels
         ZenPolicy strictPolicy = new ZenPolicy.Builder()
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+                .allowPriorityChannels(false)
                 .build();
 
         AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
@@ -3552,7 +3552,7 @@
 
         // Modifies the zen policy and device effects
         ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .allowPriorityChannels(true)
                 .build();
         ZenDeviceEffects deviceEffects =
                 new ZenDeviceEffects.Builder(rule.getDeviceEffects())
@@ -3575,8 +3575,7 @@
                 .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
         assertThat(rule.getZenPolicy().getUserModifiedFields())
                 .isEqualTo(ZenPolicy.FIELD_ALLOW_CHANNELS);
-        assertThat(rule.getZenPolicy().getAllowedChannels())
-                .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
         assertThat(rule.getDeviceEffects().getUserModifiedFields())
                 .isEqualTo(ZenDeviceEffects.FIELD_GRAYSCALE);
         assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
@@ -3864,7 +3863,7 @@
 
         // Create a fully populated ZenPolicy.
         ZenPolicy policy = new ZenPolicy.Builder()
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) // Differs from the default
+                .allowPriorityChannels(false) // Differs from the default
                 .allowReminders(true) // Differs from the default
                 .allowEvents(true) // Differs from the default
                 .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT)
@@ -3894,7 +3893,7 @@
 
         // New ZenPolicy differs from the default config
         assertThat(rule.getZenPolicy()).isNotNull();
-        assertThat(rule.getZenPolicy().getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+        assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
         assertThat(rule.canUpdate()).isFalse();
         assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(
                 ZenPolicy.FIELD_ALLOW_CHANNELS
@@ -4756,7 +4755,7 @@
                 .allowCalls(PEOPLE_TYPE_CONTACTS)
                 .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
                 .hideAllVisualEffects()
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .allowPriorityChannels(true)
                 .build();
         assertThat(mZenModeHelper.mConfig.automaticRules.values())
                 .comparingElementsUsing(IGNORE_TIMESTAMPS)
@@ -4788,7 +4787,7 @@
                 .allowCalls(PEOPLE_TYPE_STARRED)
                 .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
                 .hideAllVisualEffects()
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .allowPriorityChannels(true)
                 .build();
         assertThat(mZenModeHelper.mConfig.automaticRules.values())
                 .comparingElementsUsing(IGNORE_TIMESTAMPS)
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index 21c96d6..7941eb4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -213,13 +213,12 @@
         ZenPolicy unset = builder.build();
 
         // priority channels allowed
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        builder.allowPriorityChannels(true);
         ZenPolicy channelsPriority = builder.build();
 
         // unset applied, channels setting keeps its state
         channelsPriority.apply(unset);
-        assertThat(channelsPriority.getAllowedChannels())
-                .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(channelsPriority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
     }
 
     @Test
@@ -227,15 +226,15 @@
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
 
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE);
+        builder.allowPriorityChannels(false);
         ZenPolicy none = builder.build();
 
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        builder.allowPriorityChannels(true);
         ZenPolicy priority = builder.build();
 
         // priority channels (less strict state) cannot override a setting that sets it to none
         none.apply(priority);
-        assertThat(none.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+        assertThat(none.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
     }
 
     @Test
@@ -243,15 +242,15 @@
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
 
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE);
+        builder.allowPriorityChannels(false);
         ZenPolicy none = builder.build();
 
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        builder.allowPriorityChannels(true);
         ZenPolicy priority = builder.build();
 
         // applying a policy with channelType=none overrides priority setting
         priority.apply(none);
-        assertThat(priority.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+        assertThat(priority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
     }
 
     @Test
@@ -261,12 +260,12 @@
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
         ZenPolicy unset = builder.build();
 
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        builder.allowPriorityChannels(true);
         ZenPolicy priority = builder.build();
 
         // applying a policy with a set channel type actually goes through
         unset.apply(priority);
-        assertThat(unset.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(unset.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
     }
 
     @Test
@@ -308,7 +307,7 @@
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
 
         ZenPolicy policy = builder.build();
-        assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET);
+        assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
     }
 
     @Test
@@ -622,10 +621,10 @@
 
         // allowChannels should be unset, not be modifiable, and not show up in any output
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        builder.allowPriorityChannels(true);
         ZenPolicy policy = builder.build();
 
-        assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET);
+        assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
         assertThat(policy.toString().contains("allowChannels")).isFalse();
     }
 
@@ -635,14 +634,14 @@
 
         // allow priority channels
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        builder.allowPriorityChannels(true);
         ZenPolicy policy = builder.build();
-        assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
 
         // disallow priority channels
-        builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE);
+        builder.allowPriorityChannels(false);
         policy = builder.build();
-        assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+        assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
     }
 
     @Test
@@ -676,7 +675,7 @@
         ZenPolicy.Builder builder = new ZenPolicy.Builder();
         ZenPolicy policy = builder.allowRepeatCallers(true).allowAlarms(false)
                 .showLights(true).showBadges(false)
-                .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+                .allowPriorityChannels(true)
                 .setUserModifiedFields(20).build();
 
         ZenPolicy newPolicy = new ZenPolicy.Builder(policy).build();
@@ -689,7 +688,7 @@
         assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW);
         assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET);
 
-        assertThat(newPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+        assertThat(newPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
         assertThat(newPolicy.getUserModifiedFields()).isEqualTo(20);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 9bb2da0..ba7b52e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3365,7 +3365,7 @@
         assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
 
         if (Flags.bundleClientTransactionFlag()) {
-            verify(app2.getProcess()).scheduleClientTransactionItem(
+            verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
                     isA(WindowStateResizeItem.class));
         } else {
             verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
diff --git a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
index 2d3c4bb..2ea5dc4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
@@ -157,8 +157,11 @@
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
                 KEY_SPLASH_SCREEN_EXCEPTION_LIST, commaSeparatedList, false);
         try {
-            assertTrue("Timed out waiting for DeviceConfig to be updated.",
-                    latch.await(5, TimeUnit.SECONDS));
+            if (!latch.await(1, TimeUnit.SECONDS)) {
+                Log.w(getClass().getSimpleName(),
+                        "Timed out waiting for DeviceConfig to be updated. Force update.");
+                mList.updateDeviceConfig(commaSeparatedList);
+            }
         } catch (InterruptedException e) {
             Assert.fail(e.getMessage());
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
index 339162a..dfea2fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
@@ -44,7 +44,6 @@
 import android.view.animation.Animation;
 import android.view.animation.TranslateAnimation;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import com.android.server.AnimationThread;
@@ -147,9 +146,10 @@
         assertFinishCallbackNotCalled();
     }
 
-    @FlakyTest(bugId = 71719744)
     @Test
     public void testCancel_sneakyCancelBeforeUpdate() throws Exception {
+        final CountDownLatch animationCancelled = new CountDownLatch(1);
+
         mSurfaceAnimationRunner = new SurfaceAnimationRunner(null, () -> new ValueAnimator() {
             {
                 setFloatValues(0f, 1f);
@@ -162,6 +162,7 @@
                     // interleaving of multiple threads. Muahahaha
                     if (animation.getCurrentPlayTime() > 0) {
                         mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+                        animationCancelled.countDown();
                     }
                     listener.onAnimationUpdate(animation);
                 });
@@ -170,11 +171,7 @@
         when(mMockAnimationSpec.getDuration()).thenReturn(200L);
         mSurfaceAnimationRunner.startAnimation(mMockAnimationSpec, mMockSurface, mMockTransaction,
                 this::finishedCallback);
-
-        // We need to wait for two frames: The first frame starts the animation, the second frame
-        // actually cancels the animation.
-        waitUntilNextFrame();
-        waitUntilNextFrame();
+        assertTrue(animationCancelled.await(1, SECONDS));
         assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
         verify(mMockAnimationSpec, atLeastOnce()).apply(any(), any(), eq(0L));
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index b360800..961fdfb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1822,6 +1822,35 @@
         verify(fragment2).assignLayer(t, 2);
     }
 
+    @Test
+    public void testMoveTaskFragmentsToBottomIfNeeded() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+        final ActivityRecord unembeddedActivity = task.getTopMostActivity();
+
+        final TaskFragment fragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final TaskFragment fragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final TaskFragment fragment3 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        doReturn(true).when(fragment1).isMoveToBottomIfClearWhenLaunch();
+        doReturn(false).when(fragment2).isMoveToBottomIfClearWhenLaunch();
+        doReturn(true).when(fragment3).isMoveToBottomIfClearWhenLaunch();
+
+        assertEquals(unembeddedActivity, task.mChildren.get(0));
+        assertEquals(fragment1, task.mChildren.get(1));
+        assertEquals(fragment2, task.mChildren.get(2));
+        assertEquals(fragment3, task.mChildren.get(3));
+
+        final int[] finishCount = {0};
+        task.moveTaskFragmentsToBottomIfNeeded(unembeddedActivity, finishCount);
+
+        // fragment1 and fragment3 should be moved to the bottom of the task
+        assertEquals(fragment1, task.mChildren.get(0));
+        assertEquals(fragment3, task.mChildren.get(1));
+        assertEquals(unembeddedActivity, task.mChildren.get(2));
+        assertEquals(fragment2, task.mChildren.get(3));
+        assertEquals(2, finishCount[0]);
+    }
+
     private Task getTestTask() {
         return new TaskBuilder(mSupervisor).setCreateActivity(true).build();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 9c421ba..7ae5a11 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1062,6 +1062,8 @@
         mWm.mAnimator.ready();
         if (!mWm.mWindowPlacerLocked.isTraversalScheduled()) {
             mRootWindowContainer.performSurfacePlacement();
+        } else {
+            waitHandlerIdle(mWm.mAnimationHandler);
         }
         waitUntilWindowAnimatorIdle();
     }
diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java
index 24d3918..fe699af 100644
--- a/telecomm/java/android/telecom/CallControl.java
+++ b/telecomm/java/android/telecom/CallControl.java
@@ -19,6 +19,7 @@
 import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -32,6 +33,7 @@
 
 import com.android.internal.telecom.ClientTransactionalServiceRepository;
 import com.android.internal.telecom.ICallControl;
+import com.android.server.telecom.flags.Flags;
 
 import java.util.List;
 import java.util.Objects;
@@ -292,6 +294,43 @@
     }
 
     /**
+     * Request a new mute state.  Note: {@link CallEventCallback#onMuteStateChanged(boolean)}
+     * will be called every time the mute state is changed and can be used to track the current
+     * mute state.
+     *
+     * @param isMuted  The new mute state.  Passing in a {@link Boolean#TRUE} for the isMuted
+     *                 parameter will mute the call.  {@link Boolean#FALSE} will unmute the call.
+     * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+     *                 will be called on.
+     * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side
+     *                 that details success or failure of the requested operation.
+     *
+     *                 {@link OutcomeReceiver#onResult} will be called if Telecom has
+     *                 successfully changed the mute state.
+     *
+     *                 {@link OutcomeReceiver#onError} will be called if Telecom has failed to
+     *                 switch to the mute state.  A {@link CallException} will be
+     *                 passed that details why the operation failed.
+     */
+    @FlaggedApi(Flags.FLAG_SET_MUTE_STATE)
+    public void setMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<Void, CallException> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+        if (mServerInterface != null) {
+            try {
+                mServerInterface.setMuteState(isMuted,
+                        new CallControlResultReceiver("setMuteState", executor, callback));
+
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        } else {
+            throw new IllegalStateException(INTERFACE_ERROR_MSG);
+        }
+    }
+
+    /**
      * Raises an event to the {@link android.telecom.InCallService} implementations tracking this
      * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}.
      * These events and the associated extra keys for the {@code Bundle} parameter are mutually
diff --git a/telecomm/java/com/android/internal/telecom/ICallControl.aidl b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
index 5e2c923..372e4a12 100644
--- a/telecomm/java/com/android/internal/telecom/ICallControl.aidl
+++ b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
@@ -32,5 +32,6 @@
     void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback);
     void startCallStreaming(String callId, in ResultReceiver callback);
     void requestCallEndpointChange(in CallEndpoint callEndpoint, in ResultReceiver callback);
+    void setMuteState(boolean isMuted, in ResultReceiver callback);
     void sendEvent(String callId, String event, in Bundle extras);
 }
\ No newline at end of file
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 79e4ad0..73c26a3 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9683,6 +9683,17 @@
             "parameters_used_for_ntn_lte_signal_bar_int";
 
     /**
+     * Indicating whether plmns associated with carrier satellite can be exposed to user when
+     * manually scanning available cellular network.
+     * If key is {@code true}, satellite plmn should not be exposed to user and should be
+     * automatically set, {@code false} otherwise. Default value is {@code true}.
+     *
+     * @hide
+     */
+    public static final String KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL =
+            "remove_satellite_plmn_in_manual_network_scan_bool";
+
+    /**
      * Indicating whether DUN APN should be disabled when the device is roaming. In that case,
      * the default APN (i.e. internet) will be used for tethering.
      *
@@ -10787,6 +10798,7 @@
                 });
         sDefaults.putInt(KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT,
                 CellSignalStrengthLte.USE_RSRP);
+        sDefaults.putBoolean(KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, true);
         sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
         sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
         sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 0dce084..e4ea479 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6445,10 +6445,6 @@
      * targeting API level 31+.
      *
      * @return the current call state.
-     *
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
-     *
      * @deprecated Use {@link #getCallStateForSubscription} to retrieve the call state for a
      * specific telephony subscription (which allows carrier privileged apps),
      * {@link TelephonyCallback.CallStateListener} for real-time call state updates, or
@@ -6456,7 +6452,6 @@
      * device.
      */
     @RequiresPermission(value = android.Manifest.permission.READ_PHONE_STATE, conditional = true)
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     @Deprecated
     public @CallState int getCallState() {
         if (mContext != null) {
@@ -10782,9 +10777,7 @@
     }
 
     /**
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
-     * @deprecated Use {@link android.telecom.TelecomManager#isInCall} instead
+   * @deprecated Use {@link android.telecom.TelecomManager#isInCall} instead
      * @hide
      */
     @Deprecated
@@ -10793,15 +10786,12 @@
             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             android.Manifest.permission.READ_PHONE_STATE
     })
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     public boolean isOffhook() {
         TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE);
         return tm.isInCall();
     }
 
     /**
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
      * @deprecated Use {@link android.telecom.TelecomManager#isRinging} instead
      * @hide
      */
@@ -10811,15 +10801,12 @@
             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             android.Manifest.permission.READ_PHONE_STATE
     })
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     public boolean isRinging() {
         TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE);
         return tm.isRinging();
     }
 
     /**
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
      * @deprecated Use {@link android.telecom.TelecomManager#isInCall} instead
      * @hide
      */
@@ -10829,7 +10816,6 @@
             android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
             android.Manifest.permission.READ_PHONE_STATE
     })
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     public boolean isIdle() {
         TelecomManager tm = (TelecomManager) mContext.getSystemService(TELECOM_SERVICE);
         return !tm.isInCall();
@@ -12044,11 +12030,8 @@
      *
      * @return {@code true} if the device supports TTY mode, and {@code false} otherwise.
      *
-     * @throws UnsupportedOperationException If the device does not have
-     *          {@link PackageManager#FEATURE_TELECOM}.
      */
     @Deprecated
-    @RequiresFeature(PackageManager.FEATURE_TELECOM)
     public boolean isTtyModeSupported() {
         try {
             TelecomManager telecomManager = null;
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 2a69703..3b0397b 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -49,8 +49,10 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -2142,6 +2144,35 @@
         }
     }
 
+    /**
+     * Get all satellite PLMNs for which attach is enable for carrier.
+     *
+     * @param subId subId The subscription ID of the carrier.
+     *
+     * @return List of plmn for carrier satellite service. If no plmn is available, empty list will
+     * be returned.
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @NonNull public List<String> getAllSatellitePlmnsForCarrier(int subId) {
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            throw new IllegalArgumentException("Invalid subscription ID");
+        }
+
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.getAllSatellitePlmnsForCarrier(subId);
+            } else {
+                throw new IllegalStateException("Telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("getAllSatellitePlmnsForCarrier() RemoteException: " + ex);
+            ex.rethrowFromSystemServer();
+        }
+        return new ArrayList<>();
+    }
+
     private static ITelephony getITelephony() {
         ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
                 .getTelephonyServiceManager()
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 3ea86c7..acbf354 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3258,4 +3258,17 @@
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
         + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)")
     boolean isNullCipherNotificationsEnabled();
+
+    /**
+     * Get the aggregated satellite plmn list. This API collects plmn data from multiple sources,
+     * including carrier config, entitlement server, and config update.
+     *
+     * @param subId subId The subscription ID of the carrier.
+     *
+     * @return List of plmns for carrier satellite service. If no plmn is available, empty list will
+     * be returned.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+            + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+    List<String> getAllSatellitePlmnsForCarrier(int subId);
 }
diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java
index f5f9d97..cf38bea 100644
--- a/test-mock/src/android/test/mock/MockContext.java
+++ b/test-mock/src/android/test/mock/MockContext.java
@@ -822,12 +822,6 @@
         throw new UnsupportedOperationException();
     }
 
-    /** {@hide} */
-    @Override
-    public int getUserId() {
-        throw new UnsupportedOperationException();
-    }
-
     @Override
     public Context createConfigurationContext(Configuration overrideConfiguration) {
         throw new UnsupportedOperationException();
diff --git a/tests/Camera2Tests/CameraToo/tests/Android.bp b/tests/Camera2Tests/CameraToo/tests/Android.bp
new file mode 100644
index 0000000..8339a2c
--- /dev/null
+++ b/tests/Camera2Tests/CameraToo/tests/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2014 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: [
+        "frameworks_base_license",
+    ],
+}
+
+android_test {
+    name: "CameraTooTests",
+    instrumentation_for: "CameraToo",
+    srcs: ["src/**/*.java"],
+    sdk_version: "current",
+    static_libs: [
+        "androidx.test.rules",
+        "mockito-target-minus-junit4",
+    ],
+    data: [
+        ":CameraToo",
+    ],
+}
diff --git a/tests/Camera2Tests/CameraToo/tests/Android.mk b/tests/Camera2Tests/CameraToo/tests/Android.mk
deleted file mode 100644
index dfa64f1..0000000
--- a/tests/Camera2Tests/CameraToo/tests/Android.mk
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (C) 2014 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.
-
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := CameraTooTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../NOTICE
-LOCAL_INSTRUMENTATION_FOR := CameraToo
-LOCAL_SDK_VERSION := current
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules mockito-target-minus-junit4
-
-include $(BUILD_PACKAGE)
diff --git a/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml b/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml
new file mode 100644
index 0000000..884c095
--- /dev/null
+++ b/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs CameraToo tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CameraTooTests.apk" />
+        <option name="test-file-name" value="CameraToo.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.example.android.camera2.cameratoo.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/FsVerityTest/AndroidTest.xml b/tests/FsVerityTest/AndroidTest.xml
index d2537f6..f2d7990 100644
--- a/tests/FsVerityTest/AndroidTest.xml
+++ b/tests/FsVerityTest/AndroidTest.xml
@@ -15,6 +15,7 @@
 -->
 <configuration description="fs-verity end-to-end test">
     <option name="test-suite-tag" value="apct" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user"/>
 
     <object type="module_controller" class="com.android.tradefed.testtype.suite.module.ShippingApiLevelModuleController">
         <!-- fs-verity is required since R/30 -->
diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
index bbd4567..7343ba1c 100644
--- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.input
 
+import android.app.NotificationManager
 import android.content.Context
 import android.content.ContextWrapper
 import android.content.pm.ActivityInfo
@@ -129,6 +130,8 @@
 
     @Mock
     private lateinit var packageManager: PackageManager
+    @Mock
+    private lateinit var notificationManager: NotificationManager
     private lateinit var keyboardLayoutManager: KeyboardLayoutManager
 
     private lateinit var imeInfo: InputMethodInfo
@@ -163,6 +166,8 @@
         keyboardLayoutManager = Mockito.spy(
             KeyboardLayoutManager(context, native, dataStore, testLooper.looper)
         )
+        Mockito.`when`(context.getSystemService(Mockito.eq(Context.NOTIFICATION_SERVICE)))
+                .thenReturn(notificationManager)
         setupInputDevices()
         setupBroadcastReceiver()
         setupIme()
@@ -946,6 +951,48 @@
         }
     }
 
+    @Test
+    fun testNotificationShown_onInputDeviceChanged() {
+        val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype()))
+        Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping
+        Mockito.doReturn(false).`when`(keyboardLayoutManager).isVirtualDevice(
+            ArgumentMatchers.eq(keyboardDevice.id)
+        )
+        NewSettingsApiFlag(true).use {
+            keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id)
+            ExtendedMockito.verify(
+                notificationManager,
+                Mockito.times(1)
+            ).notifyAsUser(
+                ArgumentMatchers.isNull(),
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.any(),
+                ArgumentMatchers.any()
+            )
+        }
+    }
+
+    @Test
+    fun testNotificationNotShown_onInputDeviceChanged_forVirtualDevice() {
+        val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype()))
+        Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping
+        Mockito.doReturn(true).`when`(keyboardLayoutManager).isVirtualDevice(
+            ArgumentMatchers.eq(keyboardDevice.id)
+        )
+        NewSettingsApiFlag(true).use {
+            keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id)
+            ExtendedMockito.verify(
+                notificationManager,
+                Mockito.never()
+            ).notifyAsUser(
+                ArgumentMatchers.isNull(),
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.any(),
+                ArgumentMatchers.any()
+            )
+        }
+    }
+
     private fun assertCorrectLayout(
         device: InputDevice,
         imeSubtype: InputMethodSubtype,
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index f846164..20b7f1f 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -269,6 +269,7 @@
     @Test
     public void testCreatedTransformsAreApplied() throws Exception {
         verifyVcnTransformsApplied(mGatewayConnection, false /* expectForwardTransform */);
+        verify(mUnderlyingNetworkController).updateInboundTransform(any(), any());
     }
 
     @Test
@@ -327,6 +328,8 @@
                             eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any());
         }
 
+        verify(mUnderlyingNetworkController).updateInboundTransform(any(), any());
+
         assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
 
         final List<ChildSaProposal> saProposals =
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 4c7b25a..e29e462 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -223,6 +223,8 @@
         doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider();
         doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags();
         doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
+        doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
+        doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
 
         doReturn(mUnderlyingNetworkController)
                 .when(mDeps)
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
index 355c221..6015e931 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -26,6 +26,8 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.net.IpSecConfig;
+import android.net.IpSecTransform;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -141,4 +143,8 @@
                         mock(Handler.class));
         setupSystemService(mContext, mPowerManager, Context.POWER_SERVICE, PowerManager.class);
     }
+
+    protected IpSecTransform makeDummyIpSecTransform() throws Exception {
+        return new IpSecTransform(mContext, new IpSecConfig());
+    }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index 992f102..588624b 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -47,6 +47,8 @@
 
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.IpSecConfig;
+import android.net.IpSecTransform;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -70,6 +72,7 @@
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.NetworkBringupCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback;
 import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener;
+import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -153,11 +156,13 @@
     @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
     @Mock private UnderlyingNetworkControllerCallback mNetworkControllerCb;
+    @Mock private NetworkEvaluatorCallback mEvaluatorCallback;
     @Mock private Network mNetwork;
 
     @Spy private Dependencies mDependencies = new Dependencies();
 
     @Captor private ArgumentCaptor<UnderlyingNetworkListener> mUnderlyingNetworkListenerCaptor;
+    @Captor private ArgumentCaptor<NetworkEvaluatorCallback> mEvaluatorCallbackCaptor;
 
     private TestLooper mTestLooper;
     private VcnContext mVcnContext;
@@ -176,7 +181,7 @@
                                 mTestLooper.getLooper(),
                                 mVcnNetworkProvider,
                                 false /* isInTestMode */));
-        resetVcnContext();
+        resetVcnContext(mVcnContext);
 
         setupSystemService(
                 mContext,
@@ -202,10 +207,11 @@
                                         .getVcnUnderlyingNetworkPriorities(),
                                 SUB_GROUP,
                                 mSubscriptionSnapshot,
-                                null));
+                                null,
+                                mEvaluatorCallback));
         doReturn(mNetworkEvaluator)
                 .when(mDependencies)
-                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any());
+                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any(), any());
 
         mUnderlyingNetworkController =
                 new UnderlyingNetworkController(
@@ -217,9 +223,11 @@
                         mDependencies);
     }
 
-    private void resetVcnContext() {
-        reset(mVcnContext);
-        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
+    private void resetVcnContext(VcnContext vcnContext) {
+        reset(vcnContext);
+        doNothing().when(vcnContext).ensureRunningOnLooperThread();
+        doReturn(true).when(vcnContext).isFlagNetworkMetricMonitorEnabled();
+        doReturn(true).when(vcnContext).isFlagIpSecTransformStateEnabled();
     }
 
     // Package private for use in NetworkPriorityClassifierTest
@@ -245,11 +253,13 @@
         final ConnectivityManager cm = mock(ConnectivityManager.class);
         setupSystemService(mContext, cm, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
         final VcnContext vcnContext =
-                new VcnContext(
-                        mContext,
-                        mTestLooper.getLooper(),
-                        mVcnNetworkProvider,
-                        true /* isInTestMode */);
+                spy(
+                        new VcnContext(
+                                mContext,
+                                mTestLooper.getLooper(),
+                                mVcnNetworkProvider,
+                                true /* isInTestMode */));
+        resetVcnContext(vcnContext);
 
         new UnderlyingNetworkController(
                 vcnContext,
@@ -554,6 +564,45 @@
         verify(mNetworkEvaluator).reevaluate(any(), any(), any(), any());
     }
 
+    @Test
+    public void testUpdateIpSecTransform() {
+        verifyRegistrationOnAvailableAndGetCallback();
+
+        final UnderlyingNetworkRecord expectedRecord =
+                getTestNetworkRecord(
+                        mNetwork,
+                        INITIAL_NETWORK_CAPABILITIES,
+                        INITIAL_LINK_PROPERTIES,
+                        false /* isBlocked */);
+        final IpSecTransform expectedTransform = new IpSecTransform(mContext, new IpSecConfig());
+
+        mUnderlyingNetworkController.updateInboundTransform(expectedRecord, expectedTransform);
+        verify(mNetworkEvaluator).setInboundTransform(expectedTransform);
+    }
+
+    @Test
+    public void testOnEvaluationResultChanged() {
+        verifyRegistrationOnAvailableAndGetCallback();
+
+        // Verify #reevaluateNetworks is called by checking #getNetworkRecord
+        verify(mNetworkEvaluator).getNetworkRecord();
+
+        // Trigger the callback
+        verify(mDependencies)
+                .newUnderlyingNetworkEvaluator(
+                        any(),
+                        any(),
+                        any(),
+                        any(),
+                        any(),
+                        any(),
+                        mEvaluatorCallbackCaptor.capture());
+        mEvaluatorCallbackCaptor.getValue().onEvaluationResultChanged();
+
+        // Verify #reevaluateNetworks is called again
+        verify(mNetworkEvaluator, times(2)).getNetworkRecord();
+    }
+
     private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback() {
         return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES);
     }
@@ -682,7 +731,7 @@
 
         cb.onBlockedStatusChanged(mNetwork, true /* isBlocked */);
 
-        verifyOnSelectedUnderlyingNetworkChanged(null);
+        verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(null);
     }
 
     @Test
@@ -690,6 +739,7 @@
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
         cb.onLost(mNetwork);
+        verify(mNetworkEvaluator).close();
 
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(null);
     }
@@ -755,10 +805,11 @@
                                 underlyingNetworkTemplates,
                                 SUB_GROUP,
                                 mSubscriptionSnapshot,
-                                null));
+                                null,
+                                mEvaluatorCallback));
         doReturn(evaluator)
                 .when(mDependencies)
-                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any());
+                .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any(), any());
 
         cb.onAvailable(network);
         cb.onCapabilitiesChanged(network, responseNetworkCaps);
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
index 985e70c..aa81efe 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java
@@ -16,27 +16,65 @@
 
 package com.android.server.vcn.routeselection;
 
+import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY;
+
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_INVALID;
 import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.net.IpSecTransform;
 import android.net.vcn.VcnGatewayConnectionConfig;
-import android.os.PersistableBundle;
+
+import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
+import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.Dependencies;
+import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.concurrent.TimeUnit;
 
 public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase {
-    private PersistableBundleWrapper mCarrierConfig;
+    private static final int PENALTY_TIMEOUT_MIN = 10;
+    private static final long PENALTY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(PENALTY_TIMEOUT_MIN);
+
+    @Mock private PersistableBundleWrapper mCarrierConfig;
+    @Mock private IpSecPacketLossDetector mIpSecPacketLossDetector;
+    @Mock private Dependencies mDependencies;
+    @Mock private NetworkEvaluatorCallback mEvaluatorCallback;
+
+    @Captor private ArgumentCaptor<NetworkMetricMonitorCallback> mMetricMonitorCbCaptor;
+
+    private UnderlyingNetworkEvaluator mNetworkEvaluator;
 
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mCarrierConfig = new PersistableBundleWrapper(new PersistableBundle());
+
+        when(mDependencies.newIpSecPacketLossDetector(any(), any(), any(), any()))
+                .thenReturn(mIpSecPacketLossDetector);
+
+        when(mCarrierConfig.getIntArray(
+                        eq(VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY), anyObject()))
+                .thenReturn(new int[] {PENALTY_TIMEOUT_MIN});
+
+        mNetworkEvaluator = newValidUnderlyingNetworkEvaluator();
     }
 
     private UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator() {
@@ -46,7 +84,34 @@
                 VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
                 SUB_GROUP,
                 mSubscriptionSnapshot,
+                mCarrierConfig,
+                mEvaluatorCallback,
+                mDependencies);
+    }
+
+    private UnderlyingNetworkEvaluator newValidUnderlyingNetworkEvaluator() {
+        final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator();
+
+        evaluator.setNetworkCapabilities(
+                CELL_NETWORK_CAPABILITIES,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
                 mCarrierConfig);
+        evaluator.setLinkProperties(
+                LINK_PROPERTIES,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        evaluator.setIsBlocked(
+                false /* isBlocked */,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+
+        return evaluator;
     }
 
     @Test
@@ -98,4 +163,174 @@
         assertEquals(2, evaluator.getPriorityClass());
         assertEquals(expectedRecord, evaluator.getNetworkRecord());
     }
+
+    private void checkSetSelectedNetwork(boolean isSelected) {
+        mNetworkEvaluator.setIsSelected(
+                isSelected,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        verify(mIpSecPacketLossDetector).setIsSelectedUnderlyingNetwork(isSelected);
+    }
+
+    @Test
+    public void testSetIsSelected_selected() throws Exception {
+        checkSetSelectedNetwork(true /* isSelectedExpected */);
+    }
+
+    @Test
+    public void testSetIsSelected_unselected() throws Exception {
+        checkSetSelectedNetwork(false /* isSelectedExpected */);
+    }
+
+    @Test
+    public void testSetIpSecTransform_onSelectedNetwork() throws Exception {
+        final IpSecTransform transform = makeDummyIpSecTransform();
+
+        // Make the network selected
+        mNetworkEvaluator.setIsSelected(
+                true,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        mNetworkEvaluator.setInboundTransform(transform);
+
+        verify(mIpSecPacketLossDetector).setInboundTransform(transform);
+    }
+
+    @Test
+    public void testSetIpSecTransform_onUnSelectedNetwork() throws Exception {
+        mNetworkEvaluator.setIsSelected(
+                false,
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        mNetworkEvaluator.setInboundTransform(makeDummyIpSecTransform());
+
+        verify(mIpSecPacketLossDetector, never()).setInboundTransform(any());
+    }
+
+    @Test
+    public void close() throws Exception {
+        mNetworkEvaluator.close();
+
+        verify(mIpSecPacketLossDetector).close();
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        assertNull(mTestLooper.nextMessage());
+    }
+
+    private NetworkMetricMonitorCallback getMetricMonitorCbCaptor() throws Exception {
+        verify(mDependencies)
+                .newIpSecPacketLossDetector(any(), any(), any(), mMetricMonitorCbCaptor.capture());
+
+        return mMetricMonitorCbCaptor.getValue();
+    }
+
+    private void checkPenalizeNetwork() throws Exception {
+        assertFalse(mNetworkEvaluator.isPenalized());
+
+        // Validation failed
+        when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(true);
+        getMetricMonitorCbCaptor().onValidationResultReceived();
+
+        // Verify the evaluator is penalized
+        assertTrue(mNetworkEvaluator.isPenalized());
+        verify(mEvaluatorCallback).onEvaluationResultChanged();
+    }
+
+    @Test
+    public void testRcvValidationResult_penalizeNetwork_penaltyTimeout() throws Exception {
+        checkPenalizeNetwork();
+
+        // Penalty timeout
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        // Verify the evaluator is not penalized
+        assertFalse(mNetworkEvaluator.isPenalized());
+        verify(mEvaluatorCallback, times(2)).onEvaluationResultChanged();
+    }
+
+    @Test
+    public void testRcvValidationResult_penalizeNetwork_passValidation() throws Exception {
+        checkPenalizeNetwork();
+
+        // Validation passed
+        when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(false);
+        getMetricMonitorCbCaptor().onValidationResultReceived();
+
+        // Verify the evaluator is not penalized and penalty timeout is canceled
+        assertFalse(mNetworkEvaluator.isPenalized());
+        verify(mEvaluatorCallback, times(2)).onEvaluationResultChanged();
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        assertNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testRcvValidationResult_penalizeNetwork_closeEvaluator() throws Exception {
+        checkPenalizeNetwork();
+
+        mNetworkEvaluator.close();
+
+        // Verify penalty timeout is canceled
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        assertNull(mTestLooper.nextMessage());
+    }
+
+    @Test
+    public void testRcvValidationResult_PenaltyStateUnchanged() throws Exception {
+        assertFalse(mNetworkEvaluator.isPenalized());
+
+        // Validation passed
+        when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(false);
+        getMetricMonitorCbCaptor().onValidationResultReceived();
+
+        // Verifications
+        assertFalse(mNetworkEvaluator.isPenalized());
+        verify(mEvaluatorCallback, never()).onEvaluationResultChanged();
+    }
+
+    @Test
+    public void testSetCarrierConfig() throws Exception {
+        final int additionalTimeoutMin = 10;
+        when(mCarrierConfig.getIntArray(
+                        eq(VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY), anyObject()))
+                .thenReturn(new int[] {PENALTY_TIMEOUT_MIN + additionalTimeoutMin});
+
+        // Update evaluator and penalize the network
+        mNetworkEvaluator.reevaluate(
+                VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mCarrierConfig);
+        checkPenalizeNetwork();
+
+        // Verify penalty timeout is changed
+        mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS);
+        assertNull(mTestLooper.nextMessage());
+        mTestLooper.moveTimeForward(TimeUnit.MINUTES.toMillis(additionalTimeoutMin));
+        assertNotNull(mTestLooper.nextMessage());
+
+        // Verify NetworkMetricMonitor is notified
+        verify(mIpSecPacketLossDetector).setCarrierConfig(any());
+    }
+
+    @Test
+    public void testCompare() throws Exception {
+        when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(true);
+        getMetricMonitorCbCaptor().onValidationResultReceived();
+
+        final UnderlyingNetworkEvaluator penalized = mNetworkEvaluator;
+        final UnderlyingNetworkEvaluator notPenalized = newValidUnderlyingNetworkEvaluator();
+
+        assertEquals(penalized.getPriorityClass(), notPenalized.getPriorityClass());
+
+        final int result =
+                UnderlyingNetworkEvaluator.getComparator(mVcnContext)
+                        .compare(penalized, notPenalized);
+        assertEquals(1, result);
+    }
 }
diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk
deleted file mode 100644
index 6361f9b..0000000
--- a/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk
+++ /dev/null
@@ -1,2 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk
deleted file mode 100644
index 27b6068..0000000
--- a/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// Copyright (C) 2019 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.
-//
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_PACKAGE_NAME := AaptTestMergeOnly_App
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_EXPORT_PACKAGE_RESOURCES := true
-LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    AaptTestMergeOnly_LeafLib \
-    AaptTestMergeOnly_LocalLib
-include $(BUILD_PACKAGE)
diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk
deleted file mode 100644
index c084849..0000000
--- a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// Copyright (C) 2019 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.
-//
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestMergeOnly_LeafLib
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-LOCAL_AAPT_FLAGS := --merge-only
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk
deleted file mode 100644
index 699ad79..0000000
--- a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// Copyright (C) 2019 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.
-//
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestMergeOnly_LocalLib
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-LOCAL_AAPT_FLAGS := --merge-only
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/Android.mk
deleted file mode 100644
index 6361f9b..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/Android.mk
+++ /dev/null
@@ -1,2 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
deleted file mode 100644
index 98b7440..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-# Copyright (C) 2017 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.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_PACKAGE_NAME := AaptTestNamespace_App
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_EXPORT_PACKAGE_RESOURCES := true
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    AaptTestNamespace_LibOne \
-    AaptTestNamespace_LibTwo
-include $(BUILD_PACKAGE)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk
deleted file mode 100644
index dd41702..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-# Copyright (C) 2017 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.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestNamespace_LibOne
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-
-# We need this to retain the R.java generated for this library.
-LOCAL_JAR_EXCLUDE_FILES := none
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk
deleted file mode 100644
index 0d11bcb..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# Copyright (C) 2017 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.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestNamespace_LibTwo
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-
-# We need this to retain the R.java generated for this library.
-LOCAL_JAR_EXCLUDE_FILES := none
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk
deleted file mode 100644
index 30375728..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# Copyright (C) 2017 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.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_PACKAGE_NAME := AaptTestNamespace_Split
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_APK_LIBRARIES := AaptTestNamespace_App
-LOCAL_RES_LIBRARIES := AaptTestNamespace_App
-LOCAL_AAPT_FLAGS := --package-id 0x80 --rename-manifest-package com.android.aapt.namespace.app
-include $(BUILD_PACKAGE)