Merge "Modifying javadoc for isCameraPrivacyEnabled" into main
diff --git a/PACKAGE_MANAGER_OWNERS b/PACKAGE_MANAGER_OWNERS
index eb5842b..45719a7 100644
--- a/PACKAGE_MANAGER_OWNERS
+++ b/PACKAGE_MANAGER_OWNERS
@@ -1,3 +1,6 @@
+# Bug component: 36137
+# Bug template url: https://b.corp.google.com/issues/new?component=36137&template=198919
+
 alexbuy@google.com
 patb@google.com
 schfan@google.com
\ No newline at end of file
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 e96d07f..ee9400f 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
@@ -46,8 +46,11 @@
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccSlotMapping;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
+import android.util.IntArray;
 import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Slog;
@@ -68,6 +71,8 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Set;
 import java.util.function.Predicate;
 
 /**
@@ -1620,9 +1625,21 @@
         private final Object mSatLock = new Object();
 
         private DeviceIdleInternal mDeviceIdleInternal;
+        private TelephonyManager mTelephonyManager;
+
+        private final boolean mHasFeatureTelephonySubscription;
 
         /** Set of all apps that have been deemed special, keyed by user ID. */
         private final SparseSetArray<String> mSpecialApps = new SparseSetArray<>();
+        /**
+         * Set of carrier privileged apps, keyed by the logical ID of the SIM their privileged
+         * for.
+         */
+        @GuardedBy("mSatLock")
+        private final SparseSetArray<String> mCarrierPrivilegedApps = new SparseSetArray<>();
+        @GuardedBy("mSatLock")
+        private final SparseArray<LogicalIndexCarrierPrivilegesCallback>
+                mCarrierPrivilegedCallbacks = new SparseArray<>();
         @GuardedBy("mSatLock")
         private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>();
 
@@ -1630,6 +1647,10 @@
             @Override
             public void onReceive(Context context, Intent intent) {
                 switch (intent.getAction()) {
+                    case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
+                        updateCarrierPrivilegedCallbackRegistration();
+                        break;
+
                     case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
                         mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache);
                         break;
@@ -1637,6 +1658,11 @@
             }
         };
 
+        SpecialAppTracker() {
+            mHasFeatureTelephonySubscription = mContext.getPackageManager()
+                    .hasSystemFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION);
+        }
+
         public boolean isSpecialApp(final int userId, @NonNull String packageName) {
             synchronized (mSatLock) {
                 if (mSpecialApps.contains(UserHandle.USER_ALL, packageName)) {
@@ -1654,6 +1680,12 @@
                 if (mPowerAllowlistedApps.contains(packageName)) {
                     return true;
                 }
+                for (int l = mCarrierPrivilegedApps.size() - 1; l >= 0; --l) {
+                    if (mCarrierPrivilegedApps.contains(
+                            mCarrierPrivilegedApps.keyAt(l), packageName)) {
+                        return true;
+                    }
+                }
             }
             return false;
         }
@@ -1669,9 +1701,12 @@
 
         private void onSystemServicesReady() {
             mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class);
+            mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
 
             synchronized (mLock) {
                 if (mFlexibilityEnabled) {
+                    mHandler.post(
+                            SpecialAppTracker.this::updateCarrierPrivilegedCallbackRegistration);
                     mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache);
                 }
             }
@@ -1686,6 +1721,13 @@
         private void startTracking() {
             IntentFilter filter = new IntentFilter(
                     PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+
+            if (mHasFeatureTelephonySubscription) {
+                filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
+
+                updateCarrierPrivilegedCallbackRegistration();
+            }
+
             mContext.registerReceiver(mBroadcastReceiver, filter);
 
             updatePowerAllowlistCache();
@@ -1695,11 +1737,63 @@
             mContext.unregisterReceiver(mBroadcastReceiver);
 
             synchronized (mSatLock) {
+                mCarrierPrivilegedApps.clear();
                 mPowerAllowlistedApps.clear();
                 mSpecialApps.clear();
+
+                for (int i = mCarrierPrivilegedCallbacks.size() - 1; i >= 0; --i) {
+                    mTelephonyManager.unregisterCarrierPrivilegesCallback(
+                            mCarrierPrivilegedCallbacks.valueAt(i));
+                }
+                mCarrierPrivilegedCallbacks.clear();
             }
         }
 
+        private void updateCarrierPrivilegedCallbackRegistration() {
+            if (mTelephonyManager == null) {
+                return;
+            }
+            if (!mHasFeatureTelephonySubscription) {
+                return;
+            }
+
+            Collection<UiccSlotMapping> simSlotMapping = mTelephonyManager.getSimSlotMapping();
+            final ArraySet<String> changedPkgs = new ArraySet<>();
+            synchronized (mSatLock) {
+                final IntArray callbacksToRemove = new IntArray();
+                for (int i = mCarrierPrivilegedCallbacks.size() - 1; i >= 0; --i) {
+                    callbacksToRemove.add(mCarrierPrivilegedCallbacks.keyAt(i));
+                }
+                for (UiccSlotMapping mapping : simSlotMapping) {
+                    final int logicalIndex = mapping.getLogicalSlotIndex();
+                    if (mCarrierPrivilegedCallbacks.contains(logicalIndex)) {
+                        // Callback already exists. No need to create a new one or remove it.
+                        callbacksToRemove.remove(logicalIndex);
+                        continue;
+                    }
+                    final LogicalIndexCarrierPrivilegesCallback callback =
+                            new LogicalIndexCarrierPrivilegesCallback(logicalIndex);
+                    mCarrierPrivilegedCallbacks.put(logicalIndex, callback);
+                    // Upon registration, the callbacks will be called with the current list of
+                    // apps, so there's no need to query the app list synchronously.
+                    mTelephonyManager.registerCarrierPrivilegesCallback(logicalIndex,
+                            AppSchedulingModuleThread.getExecutor(), callback);
+                }
+
+                for (int i = callbacksToRemove.size() - 1; i >= 0; --i) {
+                    final int logicalIndex = callbacksToRemove.get(i);
+                    final LogicalIndexCarrierPrivilegesCallback callback =
+                            mCarrierPrivilegedCallbacks.get(logicalIndex);
+                    mTelephonyManager.unregisterCarrierPrivilegesCallback(callback);
+                    mCarrierPrivilegedCallbacks.remove(logicalIndex);
+                    changedPkgs.addAll(mCarrierPrivilegedApps.get(logicalIndex));
+                    mCarrierPrivilegedApps.remove(logicalIndex);
+                }
+            }
+
+            updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs);
+        }
+
         /**
          * Update the processed special app set for the specified user ID, only looking at the
          * specified set of apps. This method must <b>NEVER</b> be called while holding
@@ -1762,18 +1856,65 @@
             updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs);
         }
 
+        class LogicalIndexCarrierPrivilegesCallback implements
+                TelephonyManager.CarrierPrivilegesCallback {
+            public final int logicalIndex;
+
+            LogicalIndexCarrierPrivilegesCallback(int logicalIndex) {
+                this.logicalIndex = logicalIndex;
+            }
+
+            @Override
+            public void onCarrierPrivilegesChanged(@NonNull Set<String> privilegedPackageNames,
+                    @NonNull Set<Integer> privilegedUids) {
+                final ArraySet<String> changedPkgs = new ArraySet<>();
+                synchronized (mSatLock) {
+                    final ArraySet<String> oldPrivilegedSet =
+                            mCarrierPrivilegedApps.get(logicalIndex);
+                    if (oldPrivilegedSet != null) {
+                        changedPkgs.addAll(oldPrivilegedSet);
+                        mCarrierPrivilegedApps.remove(logicalIndex);
+                    }
+                    for (String pkgName : privilegedPackageNames) {
+                        mCarrierPrivilegedApps.add(logicalIndex, pkgName);
+                        if (!changedPkgs.remove(pkgName)) {
+                            // The package wasn't in the previous set of privileged apps. Add it
+                            // since its state has changed.
+                            changedPkgs.add(pkgName);
+                        }
+                    }
+                }
+
+                // The carrier privileged list doesn't provide a simple userId correlation,
+                // so for now, use USER_ALL for these packages.
+                // TODO(141645789): use the UID list to narrow down to specific userIds
+                updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs);
+            }
+        }
+
         public void dump(@NonNull IndentingPrintWriter pw) {
             pw.println("Special apps:");
             pw.increaseIndent();
 
             synchronized (mSatLock) {
                 for (int u = 0; u < mSpecialApps.size(); ++u) {
+                    pw.print("User ");
                     pw.print(mSpecialApps.keyAt(u));
                     pw.print(": ");
                     pw.println(mSpecialApps.valuesAt(u));
                 }
 
                 pw.println();
+                pw.println("Carrier privileged packages:");
+                pw.increaseIndent();
+                for (int i = 0; i < mCarrierPrivilegedApps.size(); ++i) {
+                    pw.print(mCarrierPrivilegedApps.keyAt(i));
+                    pw.print(": ");
+                    pw.println(mCarrierPrivilegedApps.valuesAt(i));
+                }
+                pw.decreaseIndent();
+
+                pw.println();
                 pw.print("Power allowlisted packages: ");
                 pw.println(mPowerAllowlistedApps);
             }
diff --git a/api/Android.bp b/api/Android.bp
index 8e06366..093ee4b 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -298,6 +298,28 @@
     "org.xmlpull",
 ]
 
+// These are libs from framework-internal-utils that are required (i.e. being referenced)
+// from framework-non-updatable-sources. Add more here when there's a need.
+// DO NOT add the entire framework-internal-utils. It might cause unnecessary circular
+// dependencies gets bigger.
+android_non_updatable_stubs_libs = [
+    "android.hardware.cas-V1.2-java",
+    "android.hardware.health-V1.0-java-constants",
+    "android.hardware.thermal-V1.0-java-constants",
+    "android.hardware.thermal-V2.0-java",
+    "android.hardware.tv.input-V1.0-java-constants",
+    "android.hardware.usb-V1.0-java-constants",
+    "android.hardware.usb-V1.1-java-constants",
+    "android.hardware.usb.gadget-V1.0-java",
+    "android.hardware.vibrator-V1.3-java",
+    "framework-protos",
+]
+
+java_defaults {
+    name: "android-non-updatable-stubs-libs-defaults",
+    libs: android_non_updatable_stubs_libs,
+}
+
 // Defaults for all stubs that include the non-updatable framework. These defaults do not include
 // module symbols, so will not compile correctly on their own. Users must add module APIs to the
 // classpath (or sources) somehow.
@@ -329,18 +351,7 @@
     // from framework-non-updatable-sources. Add more here when there's a need.
     // DO NOT add the entire framework-internal-utils. It might cause unnecessary circular
     // dependencies gets bigger.
-    libs: [
-        "android.hardware.cas-V1.2-java",
-        "android.hardware.health-V1.0-java-constants",
-        "android.hardware.thermal-V1.0-java-constants",
-        "android.hardware.thermal-V2.0-java",
-        "android.hardware.tv.input-V1.0-java-constants",
-        "android.hardware.usb-V1.0-java-constants",
-        "android.hardware.usb-V1.1-java-constants",
-        "android.hardware.usb.gadget-V1.0-java",
-        "android.hardware.vibrator-V1.3-java",
-        "framework-protos",
-    ],
+    libs: android_non_updatable_stubs_libs,
     flags: [
         "--error NoSettingsProvider",
         "--error UnhiddenSystemApi",
diff --git a/core/api/current.txt b/core/api/current.txt
index 42ac6b7..cd9c3ad 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -5395,7 +5395,7 @@
 
   public final class AutomaticZenRule implements android.os.Parcelable {
     ctor @Deprecated public AutomaticZenRule(String, android.content.ComponentName, android.net.Uri, int, boolean);
-    ctor public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean);
+    ctor @Deprecated public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean);
     ctor public AutomaticZenRule(android.os.Parcel);
     method public int describeContents();
     method public android.net.Uri getConditionId();
@@ -6674,9 +6674,9 @@
     method @Deprecated public android.app.Notification.Builder addPerson(String);
     method @NonNull public android.app.Notification.Builder addPerson(android.app.Person);
     method @NonNull public android.app.Notification build();
-    method public android.widget.RemoteViews createBigContentView();
-    method public android.widget.RemoteViews createContentView();
-    method public android.widget.RemoteViews createHeadsUpContentView();
+    method @Deprecated public android.widget.RemoteViews createBigContentView();
+    method @Deprecated public android.widget.RemoteViews createContentView();
+    method @Deprecated public android.widget.RemoteViews createHeadsUpContentView();
     method @NonNull public android.app.Notification.Builder extend(android.app.Notification.Extender);
     method public android.os.Bundle getExtras();
     method @Deprecated public android.app.Notification getNotification();
@@ -15708,7 +15708,7 @@
     method public boolean clipRect(int, int, int, int);
     method @FlaggedApi("com.android.graphics.hwui.flags.clip_shader") public void clipShader(@NonNull android.graphics.Shader);
     method public void concat(@Nullable android.graphics.Matrix);
-    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void concat44(@Nullable android.graphics.Matrix44);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void concat(@Nullable android.graphics.Matrix44);
     method public void disableZ();
     method public void drawARGB(int, int, int, int);
     method public void drawArc(@NonNull android.graphics.RectF, float, float, boolean, @NonNull android.graphics.Paint);
@@ -16361,7 +16361,7 @@
     ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44();
     ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44(@NonNull android.graphics.Matrix);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 concat(@NonNull android.graphics.Matrix44);
-    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public float get(int, int);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public float get(@IntRange(from=0, to=3) int, @IntRange(from=0, to=3) int);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void getValues(@NonNull float[]);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean invert();
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean isIdentity();
@@ -16370,7 +16370,7 @@
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void reset();
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 rotate(float, float, float, float);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 scale(float, float, float);
-    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void set(int, int, float);
+    method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void set(@IntRange(from=0, to=3) int, @IntRange(from=0, to=3) int, float);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void setValues(@NonNull float[]);
     method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 translate(float, float, float);
   }
@@ -41153,19 +41153,19 @@
     method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications();
     method public final void migrateNotificationFilter(int, @Nullable java.util.List<java.lang.String>);
     method public android.os.IBinder onBind(android.content.Intent);
-    method public void onInterruptionFilterChanged(int);
-    method public void onListenerConnected();
-    method public void onListenerDisconnected();
-    method public void onListenerHintsChanged(int);
-    method public void onNotificationChannelGroupModified(String, android.os.UserHandle, android.app.NotificationChannelGroup, int);
-    method public void onNotificationChannelModified(String, android.os.UserHandle, android.app.NotificationChannel, int);
-    method public void onNotificationPosted(android.service.notification.StatusBarNotification);
-    method public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
-    method public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap);
-    method public void onNotificationRemoved(android.service.notification.StatusBarNotification);
-    method public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
-    method public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap, int);
-    method public void onSilentStatusBarIconsVisibilityChanged(boolean);
+    method @UiThread public void onInterruptionFilterChanged(int);
+    method @UiThread public void onListenerConnected();
+    method @UiThread public void onListenerDisconnected();
+    method @UiThread public void onListenerHintsChanged(int);
+    method @UiThread public void onNotificationChannelGroupModified(String, android.os.UserHandle, android.app.NotificationChannelGroup, int);
+    method @UiThread public void onNotificationChannelModified(String, android.os.UserHandle, android.app.NotificationChannel, int);
+    method @UiThread public void onNotificationPosted(android.service.notification.StatusBarNotification);
+    method @UiThread public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
+    method @UiThread public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap);
+    method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification);
+    method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap);
+    method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap, int);
+    method @UiThread public void onSilentStatusBarIconsVisibilityChanged(boolean);
     method public final void requestInterruptionFilter(int);
     method public final void requestListenerHints(int);
     method public static void requestRebind(android.content.ComponentName);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index d190c62..ee69ce1 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -70,7 +70,7 @@
     field public static final String BIND_NETWORK_RECOMMENDATION_SERVICE = "android.permission.BIND_NETWORK_RECOMMENDATION_SERVICE";
     field public static final String BIND_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE";
     field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String BIND_ON_DEVICE_INTELLIGENCE_SERVICE = "android.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE";
-    field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String BIND_ON_DEVICE_TRUSTED_SERVICE = "android.permission.BIND_ON_DEVICE_TRUSTED_SERVICE";
+    field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE = "android.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE";
     field public static final String BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE = "android.permission.BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE";
     field public static final String BIND_PRINT_RECOMMENDATION_SERVICE = "android.permission.BIND_PRINT_RECOMMENDATION_SERVICE";
     field public static final String BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE = "android.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE";
@@ -2223,10 +2223,9 @@
   }
 
   public static final class Feature.Builder {
-    ctor public Feature.Builder(int, int, int, @NonNull android.os.PersistableBundle);
+    ctor public Feature.Builder(int);
     method @NonNull public android.app.ondeviceintelligence.Feature build();
     method @NonNull public android.app.ondeviceintelligence.Feature.Builder setFeatureParams(@NonNull android.os.PersistableBundle);
-    method @NonNull public android.app.ondeviceintelligence.Feature.Builder setId(int);
     method @NonNull public android.app.ondeviceintelligence.Feature.Builder setModelName(@NonNull String);
     method @NonNull public android.app.ondeviceintelligence.Feature.Builder setName(@NonNull String);
     method @NonNull public android.app.ondeviceintelligence.Feature.Builder setType(int);
@@ -2238,7 +2237,7 @@
     ctor public FeatureDetails(@android.app.ondeviceintelligence.FeatureDetails.Status int);
     method public int describeContents();
     method @NonNull public android.os.PersistableBundle getFeatureDetailParams();
-    method @android.app.ondeviceintelligence.FeatureDetails.Status public int getStatus();
+    method @android.app.ondeviceintelligence.FeatureDetails.Status public int getFeatureStatus();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.ondeviceintelligence.FeatureDetails> CREATOR;
     field public static final int FEATURE_STATUS_AVAILABLE = 3; // 0x3
@@ -2251,27 +2250,16 @@
   @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.FIELD}) public static @interface FeatureDetails.Status {
   }
 
-  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class FilePart implements android.os.Parcelable {
-    ctor public FilePart(@NonNull String, @NonNull android.os.PersistableBundle, @NonNull String) throws java.io.FileNotFoundException;
-    ctor public FilePart(@NonNull String, @NonNull android.os.PersistableBundle, @NonNull java.io.FileInputStream) throws java.io.IOException;
-    method public int describeContents();
-    method @NonNull public java.io.FileInputStream getFileInputStream();
-    method @NonNull public String getFilePartKey();
-    method @NonNull public android.os.PersistableBundle getFilePartParams();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.app.ondeviceintelligence.FilePart> CREATOR;
-  }
-
   @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public class OnDeviceIntelligenceManager {
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeature(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getFeatureDetails(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
+    method @Nullable @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public String getRemoteServicePackageName();
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void getVersion(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.LongConsumer);
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void listFeatures(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
-    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequest(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
-    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequestStreaming(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.StreamingResponseReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
+    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequest(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.ProcessingOutcomeReceiver);
+    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void processRequestStreaming(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver);
     method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestFeatureDownload(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.app.ondeviceintelligence.DownloadCallback);
-    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestTokenCount(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Long,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
-    field public static final String API_VERSION_BUNDLE_KEY = "ApiVersionBundleKey";
+    method @RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void requestTokenInfo(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.TokenInfo,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
     field public static final int REQUEST_TYPE_EMBEDDINGS = 2; // 0x2
     field public static final int REQUEST_TYPE_INFERENCE = 0; // 0x0
     field public static final int REQUEST_TYPE_PREPARE = 1; // 0x1
@@ -2305,6 +2293,10 @@
     field public static final int PROCESSING_ERROR_UNKNOWN = 1; // 0x1
   }
 
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface ProcessingOutcomeReceiver extends android.os.OutcomeReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> {
+    method public default void onDataAugmentRequest(@NonNull android.app.ondeviceintelligence.Content, @NonNull java.util.function.Consumer<android.app.ondeviceintelligence.Content>);
+  }
+
   @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class ProcessingSignal {
     ctor public ProcessingSignal();
     method public void sendSignal(@NonNull android.os.PersistableBundle);
@@ -2315,8 +2307,18 @@
     method public void onSignalReceived(@NonNull android.os.PersistableBundle);
   }
 
-  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface StreamingResponseReceiver<R, T, E extends java.lang.Throwable> extends android.os.OutcomeReceiver<R,E> {
-    method public void onNewContent(@NonNull T);
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public interface StreamedProcessingOutcomeReceiver extends android.app.ondeviceintelligence.ProcessingOutcomeReceiver {
+    method public void onNewContent(@NonNull android.app.ondeviceintelligence.Content);
+  }
+
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public final class TokenInfo implements android.os.Parcelable {
+    ctor public TokenInfo(long, @NonNull android.os.PersistableBundle);
+    ctor public TokenInfo(long);
+    method public int describeContents();
+    method public long getCount();
+    method @NonNull public android.os.PersistableBundle getInfoParams();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.ondeviceintelligence.TokenInfo> CREATOR;
   }
 
 }
@@ -12891,7 +12893,7 @@
   }
 
   public abstract class NotificationListenerService extends android.app.Service {
-    method public void onNotificationRemoved(@NonNull android.service.notification.StatusBarNotification, @NonNull android.service.notification.NotificationListenerService.RankingMap, @NonNull android.service.notification.NotificationStats, int);
+    method @UiThread public void onNotificationRemoved(@NonNull android.service.notification.StatusBarNotification, @NonNull android.service.notification.NotificationListenerService.RankingMap, @NonNull android.service.notification.NotificationStats, int);
   }
 
   public static class NotificationListenerService.Ranking {
@@ -12963,12 +12965,14 @@
   @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public abstract class OnDeviceIntelligenceService extends android.app.Service {
     ctor public OnDeviceIntelligenceService();
     method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
-    method public abstract void onDownloadFeature(@NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull android.app.ondeviceintelligence.DownloadCallback);
-    method public abstract void onGetFeature(int, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
-    method public abstract void onGetFeatureDetails(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
+    method public abstract void onDownloadFeature(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.os.CancellationSignal, @NonNull android.app.ondeviceintelligence.DownloadCallback);
+    method public abstract void onGetFeature(int, int, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Feature,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
+    method public abstract void onGetFeatureDetails(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.FeatureDetails,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
     method public abstract void onGetReadOnlyFeatureFileDescriptorMap(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.function.Consumer<java.util.Map<java.lang.String,android.os.ParcelFileDescriptor>>);
     method public abstract void onGetVersion(@NonNull java.util.function.LongConsumer);
-    method public abstract void onListFeatures(@NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
+    method public abstract void onInferenceServiceConnected();
+    method public abstract void onInferenceServiceDisconnected();
+    method public abstract void onListFeatures(int, @NonNull android.os.OutcomeReceiver<java.util.List<android.app.ondeviceintelligence.Feature>,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException>);
     method public final void updateProcessingState(@NonNull android.os.Bundle, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PersistableBundle,android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceUpdateProcessingException>);
     field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceIntelligenceService";
   }
@@ -12985,17 +12989,18 @@
     field public static final int PROCESSING_UPDATE_STATUS_CONNECTION_FAILED = 1; // 0x1
   }
 
-  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public abstract class OnDeviceTrustedInferenceService extends android.app.Service {
-    ctor public OnDeviceTrustedInferenceService();
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public abstract class OnDeviceSandboxedInferenceService extends android.app.Service {
+    ctor public OnDeviceSandboxedInferenceService();
     method public final void fetchFeatureFileInputStreamMap(@NonNull android.app.ondeviceintelligence.Feature, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.Map<java.lang.String,java.io.FileInputStream>>);
+    method @NonNull public java.util.concurrent.Executor getCallbackExecutor();
     method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
-    method @NonNull public abstract void onCountTokens(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<java.lang.Long,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
-    method @NonNull public abstract void onProcessRequest(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
-    method @NonNull public abstract void onProcessRequestStreaming(@NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.StreamingResponseReceiver<android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.Content,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
+    method @NonNull public abstract void onProcessRequest(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.ProcessingOutcomeReceiver);
+    method @NonNull public abstract void onProcessRequestStreaming(int, @NonNull android.app.ondeviceintelligence.Feature, @Nullable android.app.ondeviceintelligence.Content, int, @Nullable android.os.CancellationSignal, @Nullable android.app.ondeviceintelligence.ProcessingSignal, @NonNull android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver);
+    method @NonNull public abstract void onTokenInfoRequest(int, @NonNull android.app.ondeviceintelligence.Feature, @NonNull android.app.ondeviceintelligence.Content, @Nullable android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.ondeviceintelligence.TokenInfo,android.app.ondeviceintelligence.OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException>);
     method public abstract void onUpdateProcessingState(@NonNull android.os.Bundle, @NonNull android.os.OutcomeReceiver<android.os.PersistableBundle,android.service.ondeviceintelligence.OnDeviceIntelligenceService.OnDeviceUpdateProcessingException>);
     method public final java.io.FileInputStream openFileInput(@NonNull String) throws java.io.FileNotFoundException;
     method public final void openFileInputAsync(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.io.FileInputStream>) throws java.io.FileNotFoundException;
-    field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceTrustedInferenceService";
+    field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService";
   }
 
 }
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index 1923641..1e72a06 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -511,9 +511,9 @@
 
 InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceIntelligenceService#onBind(android.content.Intent) parameter #0:
     Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceTrustedInferenceService#onBind(android.content.Intent) parameter #0:
+InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService#onBind(android.content.Intent) parameter #0:
     Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceTrustedInferenceService#openFileInput(String) parameter #0:
+InvalidNullabilityOverride: android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService#openFileInput(String) parameter #0:
     Invalid nullability on parameter `filename` in method `openFileInput`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: android.service.textclassifier.TextClassifierService#onUnbind(android.content.Intent) parameter #0:
     Invalid nullability on parameter `intent` in method `onUnbind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
@@ -571,7 +571,7 @@
     Missing nullability on parameter `args` in method `dump`
 MissingNullability: android.service.notification.NotificationAssistantService#attachBaseContext(android.content.Context) parameter #0:
     Missing nullability on parameter `base` in method `attachBaseContext`
-MissingNullability: android.service.ondeviceintelligence.OnDeviceTrustedInferenceService#openFileInput(String):
+MissingNullability: android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService#openFileInput(String):
     Missing nullability on method `openFileInput` return
 MissingNullability: android.telephony.NetworkService#onUnbind(android.content.Intent) parameter #0:
     Missing nullability on parameter `intent` in method `onUnbind`
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index f6ec370..5e2397d 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -173,8 +173,8 @@
      *                           interrupt the user (e.g. via sound &amp; vibration) while this rule
      *                           is active.
      * @param enabled Whether the rule is enabled.
-     * @deprecated use {@link #AutomaticZenRule(String, ComponentName, ComponentName, Uri,
-     * ZenPolicy, int, boolean)}.
+     *
+     * @deprecated Use {@link AutomaticZenRule.Builder} to construct an {@link AutomaticZenRule}.
      */
     @Deprecated
     public AutomaticZenRule(String name, ComponentName owner, Uri conditionId,
@@ -206,8 +206,10 @@
      *               while this rule is active. This overrides the global policy while this rule is
      *               action ({@link Condition#STATE_TRUE}).
      * @param enabled Whether the rule is enabled.
+     *
+     * @deprecated Use {@link AutomaticZenRule.Builder} to construct an {@link AutomaticZenRule}.
      */
-    // TODO (b/309088420): deprecate this constructor in favor of the builder
+    @Deprecated
     public AutomaticZenRule(@NonNull String name, @Nullable ComponentName owner,
             @Nullable ComponentName configurationActivity, @NonNull Uri conditionId,
             @Nullable ZenPolicy policy, int interruptionFilter, boolean enabled) {
@@ -368,6 +370,9 @@
 
     /**
      * Sets the zen policy.
+     *
+     * <p>When updating an existing rule via {@link NotificationManager#updateAutomaticZenRule},
+     * a {@code null} value here means the previous policy is retained.
      */
     public void setZenPolicy(@Nullable ZenPolicy zenPolicy) {
         this.mZenPolicy = (zenPolicy == null ? null : zenPolicy.copy());
@@ -390,7 +395,12 @@
      * Sets the configuration activity - an activity that handles
      * {@link NotificationManager#ACTION_AUTOMATIC_ZEN_RULE} that shows the user more information
      * about this rule and/or allows them to configure it. This is required to be non-null for rules
-     * that are not backed by {@link android.service.notification.ConditionProviderService}.
+     * that are not backed by a {@link android.service.notification.ConditionProviderService}.
+     *
+     * <p>This is exclusive with the {@code owner} supplied in the constructor; rules where a
+     * configuration activity is set will not use the
+     * {@link android.service.notification.ConditionProviderService} supplied there to determine
+     * whether the rule should be active.
      */
     public void setConfigurationActivity(@Nullable ComponentName componentName) {
         this.configurationActivity = getTrimmedComponentName(componentName);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 1129f9d..79cb09d 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6068,11 +6068,19 @@
         }
 
         /**
-         * Construct a RemoteViews for the final 1U notification layout. In order:
-         *   1. Custom contentView from the caller
-         *   2. Style's proposed content view
-         *   3. Standard template view
+         * Construct a RemoteViews representing the standard notification layout.
+         *
+         * @deprecated For performance and system health reasons, this API is no longer required to
+         *  be used directly by the System UI when rendering Notifications to the user. While the UI
+         *  returned by this method will still represent the content of the Notification being
+         *  built, it may differ from the visual style of the system.
+         *
+         *  NOTE: this API has always had severe limitations; for example it does not support any
+         *  interactivity, it ignores the app theme, it hard-codes the colors from the system theme
+         *  at the time it is called, and it does Bitmap decoding on the main thread which can cause
+         *  UI jank.
          */
+        @Deprecated
         public RemoteViews createContentView() {
             return createContentView(false /* increasedheight */ );
         }
@@ -6181,8 +6189,19 @@
         }
 
         /**
-         * Construct a RemoteViews for the final big notification layout.
+         * Construct a RemoteViews representing the expanded notification layout.
+         *
+         * @deprecated For performance and system health reasons, this API is no longer required to
+         *  be used directly by the System UI when rendering Notifications to the user. While the UI
+         *  returned by this method will still represent the content of the Notification being
+         *  built, it may differ from the visual style of the system.
+         *
+         *  NOTE: this API has always had severe limitations; for example it does not support any
+         *  interactivity, it ignores the app theme, it hard-codes the colors from the system theme
+         *  at the time it is called, and it does Bitmap decoding on the main thread which can cause
+         *  UI jank.
          */
+        @Deprecated
         public RemoteViews createBigContentView() {
             RemoteViews result = null;
             if (useExistingRemoteView(mN.bigContentView)) {
@@ -6315,8 +6334,19 @@
         }
 
         /**
-         * Construct a RemoteViews for the final heads-up notification layout.
+         * Construct a RemoteViews representing the heads up notification layout.
+         *
+         * @deprecated For performance and system health reasons, this API is no longer required to
+         *  be used directly by the System UI when rendering Notifications to the user. While the UI
+         *  returned by this method will still represent the content of the Notification being
+         *  built, it may differ from the visual style of the system.
+         *
+         *  NOTE: this API has always had severe limitations; for example it does not support any
+         *  interactivity, it ignores the app theme, it hard-codes the colors from the system theme
+         *  at the time it is called, and it does Bitmap decoding on the main thread which can cause
+         *  UI jank.
          */
+        @Deprecated
         public RemoteViews createHeadsUpContentView() {
             return createHeadsUpContentView(false /* useIncreasedHeight */);
         }
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 283e21a..b82a1e3 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -270,13 +270,16 @@
      * Integer extra for {@link #ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED} containing the state of
      * the {@link AutomaticZenRule}.
      *
-     * <p>
-     *     The value will be one of {@link #AUTOMATIC_RULE_STATUS_ENABLED},
-     *     {@link #AUTOMATIC_RULE_STATUS_DISABLED}, {@link #AUTOMATIC_RULE_STATUS_REMOVED},
-     *     {@link #AUTOMATIC_RULE_STATUS_UNKNOWN}.
-     * </p>
+     * <p>The value will be one of {@link #AUTOMATIC_RULE_STATUS_ENABLED},
+     * {@link #AUTOMATIC_RULE_STATUS_DISABLED}, {@link #AUTOMATIC_RULE_STATUS_REMOVED},
+     * {@link #AUTOMATIC_RULE_STATUS_ACTIVATED}, {@link #AUTOMATIC_RULE_STATUS_DEACTIVATED}, or
+     * {@link #AUTOMATIC_RULE_STATUS_UNKNOWN}.
+     *
+     * <p>Note that the {@link #AUTOMATIC_RULE_STATUS_ACTIVATED} and
+     * {@link #AUTOMATIC_RULE_STATUS_DEACTIVATED} statuses are only sent to packages targeting
+     * {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above; apps targeting a lower SDK version
+     * will be sent {@link #AUTOMATIC_RULE_STATUS_UNKNOWN} in their place instead.
      */
-    // TODO (b/309101513): Add new status types to javadoc
     public static final String EXTRA_AUTOMATIC_ZEN_RULE_STATUS =
             "android.app.extra.AUTOMATIC_ZEN_RULE_STATUS";
 
@@ -370,11 +373,15 @@
             = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED";
 
     /**
-     * Intent that is broadcast when the state of getNotificationPolicy() changes.
+     * Intent that is broadcast when the state of {@link #getNotificationPolicy()} changes.
      *
      * <p>This broadcast is only sent to registered receivers and (starting from
      * {@link Build.VERSION_CODES#Q}) receivers in packages that have been granted Do Not
      * Disturb access (see {@link #isNotificationPolicyAccessGranted()}).
+     *
+     * <p>Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, most calls to
+     * {@link #setNotificationPolicy(Policy)} will update the app's implicit rule policy instead of
+     * the global policy, so this broadcast will be sent much less frequently.
      */
     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_NOTIFICATION_POLICY_CHANGED
@@ -1378,12 +1385,16 @@
     /**
      * Updates the given zen rule.
      *
-     * <p>
-     * Throws a SecurityException if policy access is not granted to this package.
+     * <p>Before {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, updating a rule that is not backed
+     * up by a {@link android.service.notification.ConditionProviderService} will deactivate it if
+     * it was previously active. Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, this
+     * will only happen if the rule's definition is actually changing.
+     *
+     * <p>Throws a SecurityException if policy access is not granted to this package.
      * See {@link #isNotificationPolicyAccessGranted}.
      *
-     * <p>
-     * Callers can only update rules that they own. See {@link AutomaticZenRule#getOwner}.
+     * <p>Callers can only update rules that they own. See {@link AutomaticZenRule#getOwner}.
+     *
      * @param id The id of the rule to update
      * @param automaticZenRule the rule to update.
      * @return Whether the rule was successfully updated.
@@ -1744,9 +1755,11 @@
     /**
      * Gets the current user-specified default notification policy.
      *
-     * <p>
+     * <p>For apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some
+     * exceptions, such as companion device managers) this method will return the policy associated
+     * to their implicit {@link AutomaticZenRule} instead, if it exists. See
+     * {@link #setNotificationPolicy(Policy)}.
      */
-    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public Policy getNotificationPolicy() {
         INotificationManager service = getService();
         try {
@@ -1757,15 +1770,20 @@
     }
 
     /**
-     * Sets the current notification policy.
+     * Sets the current notification policy (which applies when {@link #setInterruptionFilter} is
+     * called with the {@link #INTERRUPTION_FILTER_PRIORITY} value).
      *
-     * <p>
-     * Only available if policy access is granted to this package.
-     * See {@link #isNotificationPolicyAccessGranted}.
+     * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some
+     * exceptions, such as companion device managers) cannot modify the global notification policy.
+     * Calling this method will instead create or update an {@link AutomaticZenRule} associated to
+     * the app, using a {@link ZenPolicy} corresponding to the {@link Policy} supplied here, and
+     * which will be activated/deactivated by calls to {@link #setInterruptionFilter(int)}.
+     *
+     * <p>Only available if policy access is granted to this package. See
+     * {@link #isNotificationPolicyAccessGranted}.
      *
      * @param policy The new desired policy.
      */
-    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public void setNotificationPolicy(@NonNull Policy policy) {
         setNotificationPolicy(policy, /* fromUser= */ false);
     }
@@ -2786,11 +2804,17 @@
      * The interruption filter defines which notifications are allowed to
      * interrupt the user (e.g. via sound &amp; vibration) and is applied
      * globally.
-     * <p>
-     * Only available if policy access is granted to this package. See
+     *
+     * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some
+     * exceptions, such as companion device managers) cannot modify the global interruption filter.
+     * Calling this method will instead activate or deactivate an {@link AutomaticZenRule}
+     * associated to the app, using a {@link ZenPolicy} that corresponds to the {@link Policy}
+     * supplied to {@link #setNotificationPolicy(Policy)} (or the global policy when one wasn't
+     * provided).
+     *
+     * <p> Only available if policy access is granted to this package. See
      * {@link #isNotificationPolicyAccessGranted}.
      */
-    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
         setInterruptionFilter(interruptionFilter, /* fromUser= */ false);
     }
diff --git a/core/java/android/app/ondeviceintelligence/Feature.java b/core/java/android/app/ondeviceintelligence/Feature.java
index 5107354..4a38c92 100644
--- a/core/java/android/app/ondeviceintelligence/Feature.java
+++ b/core/java/android/app/ondeviceintelligence/Feature.java
@@ -199,24 +199,13 @@
 
         private long mBuilderFieldsSet = 0L;
 
-        public Builder(
-                int id,
-                int type,
-                int variant,
-                @NonNull PersistableBundle featureParams) {
+        /**
+         * Provides a builder instance to create a feature for given id.
+         * @param id the unique identifier for the feature.
+         */
+        public Builder(int id) {
             mId = id;
-            mType = type;
-            mVariant = variant;
-            mFeatureParams = featureParams;
-            com.android.internal.util.AnnotationValidations.validate(
-                    NonNull.class, null, mFeatureParams);
-        }
-
-        public @NonNull Builder setId(int value) {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x1;
-            mId = value;
-            return this;
+            mFeatureParams = new PersistableBundle();
         }
 
         public @NonNull Builder setName(@NonNull String value) {
diff --git a/core/java/android/app/ondeviceintelligence/FeatureDetails.java b/core/java/android/app/ondeviceintelligence/FeatureDetails.java
index 92f3513..f3cbd26 100644
--- a/core/java/android/app/ondeviceintelligence/FeatureDetails.java
+++ b/core/java/android/app/ondeviceintelligence/FeatureDetails.java
@@ -41,7 +41,7 @@
 @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
 public final class FeatureDetails implements Parcelable {
     @Status
-    private final int mStatus;
+    private final int mFeatureStatus;
     @NonNull
     private final PersistableBundle mFeatureDetailParams;
 
@@ -73,21 +73,21 @@
     }
 
     public FeatureDetails(
-            @Status int status,
+            @Status int featureStatus,
             @NonNull PersistableBundle featureDetailParams) {
-        this.mStatus = status;
+        this.mFeatureStatus = featureStatus;
         com.android.internal.util.AnnotationValidations.validate(
-                Status.class, null, mStatus);
+                Status.class, null, mFeatureStatus);
         this.mFeatureDetailParams = featureDetailParams;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mFeatureDetailParams);
     }
 
     public FeatureDetails(
-            @Status int status) {
-        this.mStatus = status;
+            @Status int featureStatus) {
+        this.mFeatureStatus = featureStatus;
         com.android.internal.util.AnnotationValidations.validate(
-                Status.class, null, mStatus);
+                Status.class, null, mFeatureStatus);
         this.mFeatureDetailParams = new PersistableBundle();
     }
 
@@ -95,8 +95,8 @@
     /**
      * Returns an integer value associated with the feature status.
      */
-    public @Status int getStatus() {
-        return mStatus;
+    public @Status int getFeatureStatus() {
+        return mFeatureStatus;
     }
 
 
@@ -111,7 +111,7 @@
     public String toString() {
         return MessageFormat.format("FeatureDetails '{' status = {0}, "
                         + "persistableBundle = {1} '}'",
-                mStatus,
+                mFeatureStatus,
                 mFeatureDetailParams);
     }
 
@@ -121,21 +121,21 @@
         if (o == null || getClass() != o.getClass()) return false;
         @SuppressWarnings("unchecked")
         FeatureDetails that = (FeatureDetails) o;
-        return mStatus == that.mStatus
+        return mFeatureStatus == that.mFeatureStatus
                 && java.util.Objects.equals(mFeatureDetailParams, that.mFeatureDetailParams);
     }
 
     @Override
     public int hashCode() {
         int _hash = 1;
-        _hash = 31 * _hash + mStatus;
+        _hash = 31 * _hash + mFeatureStatus;
         _hash = 31 * _hash + java.util.Objects.hashCode(mFeatureDetailParams);
         return _hash;
     }
 
     @Override
     public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
-        dest.writeInt(mStatus);
+        dest.writeInt(mFeatureStatus);
         dest.writeTypedObject(mFeatureDetailParams, flags);
     }
 
@@ -151,9 +151,9 @@
         PersistableBundle persistableBundle = (PersistableBundle) in.readTypedObject(
                 PersistableBundle.CREATOR);
 
-        this.mStatus = status;
+        this.mFeatureStatus = status;
         com.android.internal.util.AnnotationValidations.validate(
-                Status.class, null, mStatus);
+                Status.class, null, mFeatureStatus);
         this.mFeatureDetailParams = persistableBundle;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mFeatureDetailParams);
diff --git a/core/java/android/app/ondeviceintelligence/FilePart.java b/core/java/android/app/ondeviceintelligence/FilePart.java
deleted file mode 100644
index e9fb5f2..0000000
--- a/core/java/android/app/ondeviceintelligence/FilePart.java
+++ /dev/null
@@ -1,137 +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 android.app.ondeviceintelligence;
-
-import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
-
-import android.annotation.FlaggedApi;
-import android.annotation.SystemApi;
-import android.os.Parcel;
-import android.os.ParcelFileDescriptor;
-import android.os.Parcelable;
-import android.os.PersistableBundle;
-
-import android.annotation.NonNull;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.Objects;
-
-/**
- * Represents file data with an associated file descriptor sent to and received from remote
- * processing. The interface ensures that the underlying file-descriptor is always opened in
- * read-only mode.
- *
- * @hide
- */
-@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
-@SystemApi
-public final class FilePart implements Parcelable {
-    private final String mPartKey;
-    private final PersistableBundle mPartParams;
-    private final ParcelFileDescriptor mParcelFileDescriptor;
-
-    private FilePart(@NonNull String partKey, @NonNull PersistableBundle partParams,
-            @NonNull ParcelFileDescriptor parcelFileDescriptor) {
-        Objects.requireNonNull(partKey);
-        Objects.requireNonNull(partParams);
-        this.mPartKey = partKey;
-        this.mPartParams = partParams;
-        this.mParcelFileDescriptor = Objects.requireNonNull(parcelFileDescriptor);
-    }
-
-    /**
-     * Create a file part using a filePath and any additional params.
-     */
-    public FilePart(@NonNull String partKey, @NonNull PersistableBundle partParams,
-            @NonNull String filePath)
-            throws FileNotFoundException {
-        this(partKey, partParams, Objects.requireNonNull(ParcelFileDescriptor.open(
-                new File(Objects.requireNonNull(filePath)), ParcelFileDescriptor.MODE_READ_ONLY)));
-    }
-
-    /**
-     * Create a file part using a file input stream and any additional params.
-     * It is the caller's responsibility to close the stream. It is safe to do so as soon as this
-     * call returns.
-     */
-    public FilePart(@NonNull String partKey, @NonNull PersistableBundle partParams,
-            @NonNull FileInputStream fileInputStream)
-            throws IOException {
-        this(partKey, partParams, ParcelFileDescriptor.dup(fileInputStream.getFD()));
-    }
-
-    /**
-     * Returns a FileInputStream for the associated File.
-     * Caller must close the associated stream when done reading from it.
-     *
-     * @return the FileInputStream associated with the FilePart.
-     */
-    @NonNull
-    public FileInputStream getFileInputStream() {
-        return new FileInputStream(mParcelFileDescriptor.getFileDescriptor());
-    }
-
-    /**
-     * Returns the unique key associated with the part. Each Part key added to a content object
-     * should be ensured to be unique.
-     */
-    @NonNull
-    public String getFilePartKey() {
-        return mPartKey;
-    }
-
-    /**
-     * Returns the params associated with Part.
-     */
-    @NonNull
-    public PersistableBundle getFilePartParams() {
-        return mPartParams;
-    }
-
-
-    @Override
-    public int describeContents() {
-        return CONTENTS_FILE_DESCRIPTOR;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeString8(getFilePartKey());
-        dest.writePersistableBundle(getFilePartParams());
-        mParcelFileDescriptor.writeToParcel(dest, flags
-                | Parcelable.PARCELABLE_WRITE_RETURN_VALUE); // This flag ensures that the sender's
-        // copy of the Pfd is closed as soon as the Binder call succeeds.
-    }
-
-    @NonNull
-    public static final Creator<FilePart> CREATOR = new Creator<>() {
-        @Override
-        public FilePart createFromParcel(Parcel in) {
-            return new FilePart(in.readString(), in.readTypedObject(PersistableBundle.CREATOR),
-                    in.readParcelable(
-                            getClass().getClassLoader(), ParcelFileDescriptor.class));
-        }
-
-        @Override
-        public FilePart[] newArray(int size) {
-            return new FilePart[size];
-        }
-    };
-}
diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
index b925f48..360a809 100644
--- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
+++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
@@ -31,7 +31,7 @@
  import android.app.ondeviceintelligence.IResponseCallback;
  import android.app.ondeviceintelligence.IStreamingResponseCallback;
  import android.app.ondeviceintelligence.IProcessingSignal;
- import android.app.ondeviceintelligence.ITokenCountCallback;
+ import android.app.ondeviceintelligence.ITokenInfoCallback;
 
 
  /**
@@ -56,8 +56,8 @@
       void requestFeatureDownload(in Feature feature, ICancellationSignal signal, in IDownloadCallback callback) = 5;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
-      void requestTokenCount(in Feature feature, in Content request, in  ICancellationSignal signal,
-                                                        in ITokenCountCallback tokenCountcallback) = 6;
+      void requestTokenInfo(in Feature feature, in Content request, in  ICancellationSignal signal,
+                                                        in ITokenInfoCallback tokenInfocallback) = 6;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
       void processRequest(in Feature feature, in Content request, int requestType, in  ICancellationSignal cancellationSignal, in IProcessingSignal signal,
diff --git a/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl b/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl
index 9848e1d..0adf305 100644
--- a/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl
+++ b/core/java/android/app/ondeviceintelligence/IResponseCallback.aidl
@@ -3,6 +3,7 @@
 import android.app.ondeviceintelligence.Content;
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.os.PersistableBundle;
+import android.os.RemoteCallback;
 
 /**
   * Interface for a IResponseCallback for receiving response from on-device intelligence service.
@@ -12,4 +13,5 @@
 interface IResponseCallback {
     void onSuccess(in Content result) = 1;
     void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2;
+    void onDataAugmentRequest(in Content content, in RemoteCallback contentCallback) = 3;
 }
diff --git a/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl b/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
index a680574..132e53e 100644
--- a/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
+++ b/core/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
@@ -4,6 +4,7 @@
 import android.app.ondeviceintelligence.IResponseCallback;
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.os.PersistableBundle;
+import android.os.RemoteCallback;
 
 
 /**
@@ -15,4 +16,5 @@
     void onNewContent(in Content result) = 1;
     void onSuccess(in Content result) = 2;
     void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 3;
+    void onDataAugmentRequest(in Content content, in RemoteCallback contentCallback) = 4;
 }
diff --git a/core/java/android/app/ondeviceintelligence/ITokenCountCallback.aidl b/core/java/android/app/ondeviceintelligence/ITokenCountCallback.aidl
deleted file mode 100644
index b724e03..0000000
--- a/core/java/android/app/ondeviceintelligence/ITokenCountCallback.aidl
+++ /dev/null
@@ -1,13 +0,0 @@
-package android.app.ondeviceintelligence;
-
-import android.os.PersistableBundle;
-
-/**
-  * Interface for receiving the token count of a request for a given features.
-  *
-  * @hide
-  */
-interface ITokenCountCallback {
-    void onSuccess(long tokenCount) = 1;
-    void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2;
-}
diff --git a/core/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl b/core/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
new file mode 100644
index 0000000..9219a89
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
@@ -0,0 +1,14 @@
+package android.app.ondeviceintelligence;
+
+import android.os.PersistableBundle;
+import android.app.ondeviceintelligence.TokenInfo;
+
+/**
+  * Interface for receiving the token info of a request for a given feature.
+  *
+  * @hide
+  */
+interface ITokenInfoCallback {
+    void onSuccess(in TokenInfo tokenInfo) = 1;
+    void onFailure(int errorCode, in String errorMessage, in PersistableBundle errorParams) = 2;
+}
diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
index 4d8e0d5..d195c4d 100644
--- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
+++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
@@ -26,8 +26,10 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.content.ComponentName;
 import android.content.Context;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
@@ -37,6 +39,8 @@
 
 import androidx.annotation.IntDef;
 
+import com.android.internal.R;
+
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -60,7 +64,16 @@
 @SystemService(Context.ON_DEVICE_INTELLIGENCE_SERVICE)
 @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
 public class OnDeviceIntelligenceManager {
+    /**
+     * @hide
+     */
     public static final String API_VERSION_BUNDLE_KEY = "ApiVersionBundleKey";
+
+    /**
+     * @hide
+     */
+    public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY =
+            "AugmentRequestContentBundleKey";
     private final Context mContext;
     private final IOnDeviceIntelligenceManager mService;
 
@@ -82,8 +95,6 @@
     public void getVersion(
             @NonNull @CallbackExecutor Executor callbackExecutor,
             @NonNull LongConsumer versionConsumer) {
-        // TODO explore modifying this method into getServicePackageDetails and return both
-        //  version and package name of the remote service implementing this.
         try {
             RemoteCallback callback = new RemoteCallback(result -> {
                 if (result == null) {
@@ -100,6 +111,23 @@
         }
     }
 
+
+    /**
+     * Get package name configured for providing the remote implementation for this system service.
+     */
+    @Nullable
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public String getRemoteServicePackageName() {
+        String serviceConfigValue = mContext.getResources().getString(
+                R.string.config_defaultOnDeviceSandboxedInferenceService);
+        ComponentName componentName = ComponentName.unflattenFromString(serviceConfigValue);
+        if (componentName != null) {
+            return componentName.getPackageName();
+        }
+
+        return null;
+    }
+
     /**
      * Asynchronously get feature for a given id.
      *
@@ -273,29 +301,29 @@
     }
 
     /**
-     * The methods computes the token-count for a given request payload using the provided Feature
-     * details.
+     * The methods computes the token related information for a given request payload using the
+     * provided {@link Feature}.
      *
      * @param feature            feature associated with the request.
      * @param request            request that contains the content data and associated params.
-     * @param outcomeReceiver    callback to populate the token count or exception in case of
+     * @param outcomeReceiver    callback to populate the token info or exception in case of
      *                           failure.
      * @param cancellationSignal signal to invoke cancellation on the operation in the remote
      *                           implementation.
      * @param callbackExecutor   executor to run the callback on.
      */
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
-    public void requestTokenCount(@NonNull Feature feature, @NonNull Content request,
+    public void requestTokenInfo(@NonNull Feature feature, @NonNull Content request,
             @Nullable CancellationSignal cancellationSignal,
             @NonNull @CallbackExecutor Executor callbackExecutor,
-            @NonNull OutcomeReceiver<Long,
+            @NonNull OutcomeReceiver<TokenInfo,
                     OnDeviceIntelligenceManagerException> outcomeReceiver) {
         try {
-            ITokenCountCallback callback = new ITokenCountCallback.Stub() {
+            ITokenInfoCallback callback = new ITokenInfoCallback.Stub() {
                 @Override
-                public void onSuccess(long tokenCount) {
+                public void onSuccess(TokenInfo tokenInfo) {
                     Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
-                            () -> outcomeReceiver.onResult(tokenCount)));
+                            () -> outcomeReceiver.onResult(tokenInfo)));
                 }
 
                 @Override
@@ -314,7 +342,7 @@
                 cancellationSignal.setRemote(transport);
             }
 
-            mService.requestTokenCount(feature, request, transport, callback);
+            mService.requestTokenInfo(feature, request, transport, callback);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -328,33 +356,31 @@
      * was a
      * failure.
      *
-     * @param feature                 feature associated with the request.
-     * @param request                 request that contains the Content data and
-     *                                associated params.
-     * @param requestType             type of request being sent for processing the content.
-     * @param responseOutcomeReceiver callback to populate the response content and
-     *                                associated
-     *                                params.
-     * @param processingSignal        signal to invoke custom actions in the
-     *                                remote implementation.
-     * @param cancellationSignal      signal to invoke cancellation or
-     * @param callbackExecutor        executor to run the callback on.
+     * @param feature            feature associated with the request.
+     * @param request            request that contains the Content data and
+     *                           associated params.
+     * @param requestType        type of request being sent for processing the content.
+     * @param cancellationSignal signal to invoke cancellation.
+     * @param processingSignal   signal to send custom signals in the
+     *                           remote implementation.
+     * @param callbackExecutor   executor to run the callback on.
+     * @param responseCallback   callback to populate the response content and
+     *                           associated params.
      */
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
 
-    public void processRequest(@NonNull Feature feature, @NonNull Content request,
+    public void processRequest(@NonNull Feature feature, @Nullable Content request,
             @RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
             @NonNull @CallbackExecutor Executor callbackExecutor,
-            @NonNull OutcomeReceiver<Content,
-                    OnDeviceIntelligenceManagerProcessingException> responseOutcomeReceiver) {
+            @NonNull ProcessingOutcomeReceiver responseCallback) {
         try {
             IResponseCallback callback = new IResponseCallback.Stub() {
                 @Override
                 public void onSuccess(Content result) {
                     Binder.withCleanCallingIdentity(() -> {
-                        callbackExecutor.execute(() -> responseOutcomeReceiver.onResult(result));
+                        callbackExecutor.execute(() -> responseCallback.onResult(result));
                     });
                 }
 
@@ -362,12 +388,24 @@
                 public void onFailure(int errorCode, String errorMessage,
                         PersistableBundle errorParams) {
                     Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
-                            () -> responseOutcomeReceiver.onError(
+                            () -> responseCallback.onError(
                                     new OnDeviceIntelligenceManagerProcessingException(
                                             errorCode, errorMessage, errorParams))));
                 }
+
+                @Override
+                public void onDataAugmentRequest(@NonNull Content content,
+                        @NonNull RemoteCallback contentCallback) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> responseCallback.onDataAugmentRequest(content, result -> {
+                                Bundle bundle = new Bundle();
+                                bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, result);
+                                callbackExecutor.execute(() -> contentCallback.sendResult(bundle));
+                            })));
+                }
             };
 
+
             IProcessingSignal transport = null;
             if (processingSignal != null) {
                 transport = ProcessingSignal.createTransport();
@@ -389,46 +427,48 @@
     }
 
     /**
-     * Variation of {@link #processRequest} that asynchronously processes a request in a streaming
+     * Variation of {@link #processRequest} that asynchronously processes a request in a
+     * streaming
      * fashion, where new content is pushed to caller in chunks via the
-     * {@link StreamingResponseReceiver#onNewContent}. After the streaming is complete,
-     * the service should call {@link StreamingResponseReceiver#onResult} and can optionally
-     * populate the complete {@link Response}'s Content as part of the callback when the final
-     * {@link Response} contains an enhanced aggregation of the Contents already streamed.
+     * {@link StreamedProcessingOutcomeReceiver#onNewContent}. After the streaming is complete,
+     * the service should call {@link StreamedProcessingOutcomeReceiver#onResult} and can optionally
+     * populate the complete the full response {@link Content} as part of the callback in cases
+     * when the final response contains an enhanced aggregation of the Contents already
+     * streamed.
      *
      * @param feature                   feature associated with the request.
      * @param request                   request that contains the Content data and associated
      *                                  params.
      * @param requestType               type of request being sent for processing the content.
-     * @param processingSignal          signal to invoke  other custom actions in the
+     * @param cancellationSignal        signal to invoke cancellation.
+     * @param processingSignal          signal to send custom signals in the
      *                                  remote implementation.
-     * @param cancellationSignal        signal to invoke cancellation
-     * @param streamingResponseReceiver streaming callback to populate the response content and
+     * @param streamingResponseCallback streaming callback to populate the response content and
      *                                  associated params.
      * @param callbackExecutor          executor to run the callback on.
      */
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
-    public void processRequestStreaming(@NonNull Feature feature, @NonNull Content request,
+    public void processRequestStreaming(@NonNull Feature feature, @Nullable Content request,
             @RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
             @NonNull @CallbackExecutor Executor callbackExecutor,
-            @NonNull StreamingResponseReceiver<Content, Content,
-                    OnDeviceIntelligenceManagerProcessingException> streamingResponseReceiver) {
+            @NonNull StreamedProcessingOutcomeReceiver streamingResponseCallback) {
         try {
             IStreamingResponseCallback callback = new IStreamingResponseCallback.Stub() {
                 @Override
                 public void onNewContent(Content result) {
                     Binder.withCleanCallingIdentity(() -> {
                         callbackExecutor.execute(
-                                () -> streamingResponseReceiver.onNewContent(result));
+                                () -> streamingResponseCallback.onNewContent(result));
                     });
                 }
 
                 @Override
                 public void onSuccess(Content result) {
                     Binder.withCleanCallingIdentity(() -> {
-                        callbackExecutor.execute(() -> streamingResponseReceiver.onResult(result));
+                        callbackExecutor.execute(
+                                () -> streamingResponseCallback.onResult(result));
                     });
                 }
 
@@ -437,11 +477,26 @@
                         PersistableBundle errorParams) {
                     Binder.withCleanCallingIdentity(() -> {
                         callbackExecutor.execute(
-                                () -> streamingResponseReceiver.onError(
+                                () -> streamingResponseCallback.onError(
                                         new OnDeviceIntelligenceManagerProcessingException(
                                                 errorCode, errorMessage, errorParams)));
                     });
                 }
+
+
+                @Override
+                public void onDataAugmentRequest(@NonNull Content content,
+                        @NonNull RemoteCallback contentCallback) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> streamingResponseCallback.onDataAugmentRequest(content,
+                                    contentResponse -> {
+                                        Bundle bundle = new Bundle();
+                                        bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY,
+                                                contentResponse);
+                                        callbackExecutor.execute(
+                                                () -> contentCallback.sendResult(bundle));
+                                    })));
+                }
             };
 
             IProcessingSignal transport = null;
@@ -468,7 +523,8 @@
     public static final int REQUEST_TYPE_INFERENCE = 0;
 
     /**
-     * Prepares the remote implementation environment for e.g.loading inference runtime etc.which
+     * Prepares the remote implementation environment for e.g.loading inference runtime etc
+     * .which
      * are time consuming beforehand to remove overhead and allow quick processing of requests
      * thereof.
      */
@@ -485,7 +541,8 @@
             REQUEST_TYPE_PREPARE,
             REQUEST_TYPE_EMBEDDINGS
     }, open = true)
-    @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
+    @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER,
+            ElementType.FIELD})
     @Retention(RetentionPolicy.SOURCE)
     public @interface RequestType {
     }
@@ -501,17 +558,32 @@
          */
         public static final int ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE = 1000;
 
+        /**
+         * Error code to be used for on device intelligence manager failures.
+         *
+         * @hide
+         */
+        @IntDef(
+                value = {
+                        ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE
+                }, open = true)
+        @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+        @interface OnDeviceIntelligenceManagerErrorCode {
+        }
+
         private final int mErrorCode;
         private final PersistableBundle errorParams;
 
-        public OnDeviceIntelligenceManagerException(int errorCode, @NonNull String errorMessage,
+        public OnDeviceIntelligenceManagerException(
+                @OnDeviceIntelligenceManagerErrorCode int errorCode, @NonNull String errorMessage,
                 @NonNull PersistableBundle errorParams) {
             super(errorMessage);
             this.mErrorCode = errorCode;
             this.errorParams = errorParams;
         }
 
-        public OnDeviceIntelligenceManagerException(int errorCode,
+        public OnDeviceIntelligenceManagerException(
+                @OnDeviceIntelligenceManagerErrorCode int errorCode,
                 @NonNull PersistableBundle errorParams) {
             this.mErrorCode = errorCode;
             this.errorParams = errorParams;
@@ -573,11 +645,15 @@
         /** Inference suspended so that higher-priority inference can run. */
         public static final int PROCESSING_ERROR_SUSPENDED = 13;
 
-        /** Underlying processing encountered an internal error, like a violated precondition. */
+        /**
+         * Underlying processing encountered an internal error, like a violated precondition
+         * .
+         */
         public static final int PROCESSING_ERROR_INTERNAL = 14;
 
         /**
-         * The processing was not able to be passed on to the remote implementation, as the service
+         * The processing was not able to be passed on to the remote implementation, as the
+         * service
          * was unavailable.
          */
         public static final int PROCESSING_ERROR_SERVICE_UNAVAILABLE = 15;
diff --git a/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java b/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java
new file mode 100644
index 0000000..b0b6e19
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/ProcessingOutcomeReceiver.java
@@ -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 android.app.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.OutcomeReceiver;
+
+import java.util.function.Consumer;
+
+/**
+ * Response Callback to populate the processed response or any error that occurred during the
+ * request processing. This callback also provides a method to request additional data to be
+ * augmented to the request-processing, using  the partial {@link Content} that was already
+ * processed in the remote implementation.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public interface ProcessingOutcomeReceiver extends
+        OutcomeReceiver<Content,
+                OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> {
+    /**
+     * Callback to be invoked in cases where the remote service needs to perform retrieval or
+     * transformation operations based on a partially processed request, in order to augment the
+     * final response, by using the additional context sent via this callback.
+     *
+     * @param content         The content payload that should be used to augment ongoing request.
+     * @param contentConsumer The augmentation data that should be sent to remote
+     *                        service for further processing a request.
+     */
+    default void onDataAugmentRequest(@NonNull Content content,
+            @NonNull Consumer<Content> contentConsumer) {
+        contentConsumer.accept(null);
+    }
+}
diff --git a/core/java/android/app/ondeviceintelligence/StreamingResponseReceiver.java b/core/java/android/app/ondeviceintelligence/StreamedProcessingOutcomeReceiver.java
similarity index 71%
rename from core/java/android/app/ondeviceintelligence/StreamingResponseReceiver.java
rename to core/java/android/app/ondeviceintelligence/StreamedProcessingOutcomeReceiver.java
index ebcf61c..ac2b032 100644
--- a/core/java/android/app/ondeviceintelligence/StreamingResponseReceiver.java
+++ b/core/java/android/app/ondeviceintelligence/StreamedProcessingOutcomeReceiver.java
@@ -21,23 +21,19 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
-import android.os.OutcomeReceiver;
 
 /**
  * Streaming variant of outcome receiver to populate response while processing a given request,
- * possibly in
- * chunks to provide a async processing behaviour to the caller.
+ * possibly in chunks to provide a async processing behaviour to the caller.
  *
  * @hide
  */
 @SystemApi
 @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
-public interface StreamingResponseReceiver<R, T, E extends Throwable> extends
-        OutcomeReceiver<R, E> {
+public interface StreamedProcessingOutcomeReceiver extends ProcessingOutcomeReceiver {
     /**
-     * Callback to be invoked when a part of the response i.e. some {@link Content} is already
-     * processed and
-     * needs to be passed onto the caller.
+     * Callback that would be invoked when a part of the response i.e. some {@link Content} is
+     * already processed and needs to be passed onto the caller.
      */
-    void onNewContent(@NonNull T content);
+    void onNewContent(@NonNull Content content);
 }
diff --git a/core/java/android/app/ondeviceintelligence/TokenInfo.aidl b/core/java/android/app/ondeviceintelligence/TokenInfo.aidl
new file mode 100644
index 0000000..2c19c1e
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/TokenInfo.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.app.ondeviceintelligence;
+
+/**
+  * @hide
+  */
+parcelable TokenInfo;
diff --git a/core/java/android/app/ondeviceintelligence/TokenInfo.java b/core/java/android/app/ondeviceintelligence/TokenInfo.java
new file mode 100644
index 0000000..035cc4b
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/TokenInfo.java
@@ -0,0 +1,94 @@
+/*
+ * 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.app.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+
+/**
+ * This class is used to provide a token count response for the
+ * {@link OnDeviceIntelligenceManager#requestTokenInfo} outcome receiver.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public final class TokenInfo implements Parcelable {
+    private final long mCount;
+    private final PersistableBundle mInfoParams;
+
+    /**
+     * Construct a token count using the count value and associated params.
+     */
+    public TokenInfo(long count, @NonNull PersistableBundle persistableBundle) {
+        this.mCount = count;
+        mInfoParams = persistableBundle;
+    }
+
+    /**
+     * Construct a token count using the count value.
+     */
+    public TokenInfo(long count) {
+        this.mCount = count;
+        this.mInfoParams = new PersistableBundle();
+    }
+
+    /**
+     * Returns the token count associated with a request payload.
+     */
+    public long getCount() {
+        return mCount;
+    }
+
+    /**
+     * Returns the params representing token info.
+     */
+    @NonNull
+    public PersistableBundle getInfoParams() {
+        return mInfoParams;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeLong(mCount);
+        dest.writePersistableBundle(mInfoParams);
+    }
+
+    public static final @NonNull Parcelable.Creator<TokenInfo> CREATOR
+            = new Parcelable.Creator<>() {
+        @Override
+        public TokenInfo[] newArray(int size) {
+            return new TokenInfo[size];
+        }
+
+        @Override
+        public TokenInfo createFromParcel(@NonNull Parcel in) {
+            return new TokenInfo(in.readLong(), in.readPersistableBundle());
+        }
+    };
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index eea6464..cdb35ff 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -19992,6 +19992,13 @@
             @Readable
             public static final String CONSISTENT_NOTIFICATION_BLOCKING_ENABLED =
                     "consistent_notification_blocking_enabled";
+
+            /**
+             * Whether the Auto Bedtime Mode experience is enabled.
+             *
+             * @hide
+             */
+            public static final String AUTO_BEDTIME_MODE = "auto_bedtime_mode";
         }
     }
 
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 7658af5..bd9ab86 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
 import android.annotation.SystemApi;
+import android.annotation.UiThread;
 import android.app.ActivityManager;
 import android.app.INotificationManager;
 import android.app.Notification;
@@ -468,6 +469,7 @@
      *            object as well as its identifying information (tag and id) and source
      *            (package name).
      */
+    @UiThread
     public void onNotificationPosted(StatusBarNotification sbn) {
         // optional
     }
@@ -481,6 +483,7 @@
      * @param rankingMap The current ranking map that can be used to retrieve ranking information
      *                   for active notifications, including the newly posted one.
      */
+    @UiThread
     public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
         onNotificationPosted(sbn);
     }
@@ -499,6 +502,7 @@
      *            and source (package name) used to post the {@link android.app.Notification} that
      *            was just removed.
      */
+    @UiThread
     public void onNotificationRemoved(StatusBarNotification sbn) {
         // optional
     }
@@ -520,6 +524,7 @@
      *                   for active notifications.
      *
      */
+    @UiThread
     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
         onNotificationRemoved(sbn);
     }
@@ -541,6 +546,7 @@
      * @param rankingMap The current ranking map that can be used to retrieve ranking information
      *                   for active notifications.
      */
+    @UiThread
     public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
             @NotificationCancelReason int reason) {
         onNotificationRemoved(sbn, rankingMap);
@@ -552,6 +558,7 @@
      *
      * @hide
      */
+    @UiThread
     @SystemApi
     public void onNotificationRemoved(@NonNull StatusBarNotification sbn,
             @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason) {
@@ -563,6 +570,7 @@
      * the notification manager.  You are safe to call {@link #getActiveNotifications()}
      * at this time.
      */
+    @UiThread
     public void onListenerConnected() {
         // optional
     }
@@ -572,6 +580,7 @@
      * notification manager.You will not receive any events after this call, and may only
      * call {@link #requestRebind(ComponentName)} at this time.
      */
+    @UiThread
     public void onListenerDisconnected() {
         // optional
     }
@@ -582,6 +591,7 @@
      * @param rankingMap The current ranking map that can be used to retrieve ranking information
      *                   for active notifications.
      */
+    @UiThread
     public void onNotificationRankingUpdate(RankingMap rankingMap) {
         // optional
     }
@@ -592,6 +602,7 @@
      *
      * @param hints The current {@link #getCurrentListenerHints() listener hints}.
      */
+    @UiThread
     public void onListenerHintsChanged(int hints) {
         // optional
     }
@@ -603,6 +614,7 @@
      * @param hideSilentStatusIcons whether or not status bar icons should be hidden for silent
      *                              notifications
      */
+    @UiThread
     public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) {
         // optional
     }
@@ -620,6 +632,7 @@
      *                   {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED},
      *                   {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}.
      */
+    @UiThread
     public void onNotificationChannelModified(String pkg, UserHandle user,
             NotificationChannel channel, @ChannelOrGroupModificationTypes int modificationType) {
         // optional
@@ -638,6 +651,7 @@
      *                   {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED},
      *                   {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}.
      */
+    @UiThread
     public void onNotificationChannelGroupModified(String pkg, UserHandle user,
             NotificationChannelGroup group, @ChannelOrGroupModificationTypes int modificationType) {
         // optional
@@ -650,6 +664,7 @@
      * @param interruptionFilter The current
      *     {@link #getCurrentInterruptionFilter() interruption filter}.
      */
+    @UiThread
     public void onInterruptionFilterChanged(int interruptionFilter) {
         // optional
     }
@@ -1197,6 +1212,11 @@
      * <p>
      * Listen for updates using {@link #onInterruptionFilterChanged(int)}.
      *
+     * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some
+     * exceptions, such as companion device managers) cannot modify the global interruption filter.
+     * Calling this method will instead activate or deactivate an
+     * {@link android.app.AutomaticZenRule} associated to the app.
+     *
      * <p>The service should wait for the {@link #onListenerConnected()} event
      * before performing this operation.
      *
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
index e44c69c..6dbff71 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
@@ -36,11 +36,13 @@
  */
 oneway interface IOnDeviceIntelligenceService {
     void getVersion(in RemoteCallback remoteCallback);
-    void getFeature(in int featureId, in IFeatureCallback featureCallback);
-    void listFeatures(in IListFeaturesCallback listFeaturesCallback);
-    void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback);
+    void getFeature(int callerUid, int featureId, in IFeatureCallback featureCallback);
+    void listFeatures(int callerUid, in IListFeaturesCallback listFeaturesCallback);
+    void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback);
     void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future);
     void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback);
-    void requestFeatureDownload(in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback);
+    void requestFeatureDownload(int callerUid, in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback);
     void registerRemoteServices(in IRemoteProcessingService remoteProcessingService);
+    void notifyInferenceServiceConnected();
+    void notifyInferenceServiceDisconnected();
 }
\ No newline at end of file
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceTrustedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
similarity index 74%
rename from core/java/android/service/ondeviceintelligence/IOnDeviceTrustedInferenceService.aidl
rename to core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
index e3fda04..73257ed 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceTrustedInferenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
@@ -18,7 +18,7 @@
 
 import android.app.ondeviceintelligence.IStreamingResponseCallback;
 import android.app.ondeviceintelligence.IResponseCallback;
-import android.app.ondeviceintelligence.ITokenCountCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.app.ondeviceintelligence.Content;
 import android.app.ondeviceintelligence.Feature;
@@ -29,18 +29,18 @@
 import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
 
 /**
- * Interface for a concrete implementation to provide on device trusted inference.
+ * Interface for a concrete implementation to provide on-device sandboxed inference.
  *
  * @hide
  */
-oneway interface IOnDeviceTrustedInferenceService {
+oneway interface IOnDeviceSandboxedInferenceService {
     void registerRemoteStorageService(in IRemoteStorageService storageService);
-    void requestTokenCount(in Feature feature, in Content request, in ICancellationSignal cancellationSignal,
-                            in ITokenCountCallback tokenCountCallback);
-    void processRequest(in Feature feature, in Content request, in int requestType,
+    void requestTokenInfo(int callerUid, in Feature feature, in Content request, in ICancellationSignal cancellationSignal,
+                            in ITokenInfoCallback tokenInfoCallback);
+    void processRequest(int callerUid, in Feature feature, in Content request, in int requestType,
                         in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
                         in IResponseCallback callback);
-    void processRequestStreaming(in Feature feature, in Content request, in int requestType,
+    void processRequestStreaming(int callerUid, in Feature feature, in Content request, in int requestType,
                                 in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
                                 in IStreamingResponseCallback callback);
     void updateProcessingState(in Bundle processingState,
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
index 46ba25d..fce3689 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
@@ -65,7 +65,7 @@
 
 /**
  * Abstract base class for performing setup for on-device inference and providing file access to
- * the isolated counter part {@link OnDeviceTrustedInferenceService}.
+ * the isolated counter part {@link OnDeviceSandboxedInferenceService}.
  *
  * <p> A service that provides configuration and model files relevant to performing inference on
  * device. The system's default OnDeviceIntelligenceService implementation is configured in
@@ -110,6 +110,8 @@
     @Override
     public final IBinder onBind(@NonNull Intent intent) {
         if (SERVICE_INTERFACE.equals(intent.getAction())) {
+            // TODO(326052028) : Move the remote method calls to an app handler from the binder
+            //  thread.
             return new IOnDeviceIntelligenceService.Stub() {
                 /** {@inheritDoc} */
                 @Override
@@ -123,38 +125,40 @@
                 }
 
                 @Override
-                public void listFeatures(IListFeaturesCallback listFeaturesCallback) {
+                public void listFeatures(int callerUid,
+                        IListFeaturesCallback listFeaturesCallback) {
                     Objects.requireNonNull(listFeaturesCallback);
-                    OnDeviceIntelligenceService.this.onListFeatures(
+                    OnDeviceIntelligenceService.this.onListFeatures(callerUid,
                             wrapListFeaturesCallback(listFeaturesCallback));
                 }
 
                 @Override
-                public void getFeature(int id, IFeatureCallback featureCallback) {
+                public void getFeature(int callerUid, int id, IFeatureCallback featureCallback) {
                     Objects.requireNonNull(featureCallback);
-                    OnDeviceIntelligenceService.this.onGetFeature(id,
-                            wrapFeatureCallback(featureCallback));
+                    OnDeviceIntelligenceService.this.onGetFeature(callerUid,
+                            id, wrapFeatureCallback(featureCallback));
                 }
 
 
                 @Override
-                public void getFeatureDetails(Feature feature,
+                public void getFeatureDetails(int callerUid, Feature feature,
                         IFeatureDetailsCallback featureDetailsCallback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(featureDetailsCallback);
 
-                    OnDeviceIntelligenceService.this.onGetFeatureDetails(feature,
-                            wrapFeatureDetailsCallback(featureDetailsCallback));
+                    OnDeviceIntelligenceService.this.onGetFeatureDetails(callerUid,
+                            feature, wrapFeatureDetailsCallback(featureDetailsCallback));
                 }
 
                 @Override
-                public void requestFeatureDownload(Feature feature,
+                public void requestFeatureDownload(int callerUid, Feature feature,
                         ICancellationSignal cancellationSignal,
                         IDownloadCallback downloadCallback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(downloadCallback);
 
-                    OnDeviceIntelligenceService.this.onDownloadFeature(feature,
+                    OnDeviceIntelligenceService.this.onDownloadFeature(callerUid,
+                            feature,
                             CancellationSignal.fromTransport(cancellationSignal),
                             wrapDownloadCallback(downloadCallback));
                 }
@@ -188,12 +192,38 @@
                         IRemoteProcessingService remoteProcessingService) {
                     mRemoteProcessingService = remoteProcessingService;
                 }
+
+                @Override
+                public void notifyInferenceServiceConnected() {
+                    OnDeviceIntelligenceService.this.onInferenceServiceConnected();
+                }
+
+                @Override
+                public void notifyInferenceServiceDisconnected() {
+                    OnDeviceIntelligenceService.this.onInferenceServiceDisconnected();
+                }
             };
         }
         Slog.w(TAG, "Incorrect service interface, returning null.");
         return null;
     }
 
+
+    /**
+     * Invoked when a new instance of the remote inference service is created.
+     * This method should be used as a signal to perform any initialization operations, for e.g. by
+     * invoking the {@link #updateProcessingState} method to initialize the remote processing
+     * service.
+     */
+    public abstract void onInferenceServiceConnected();
+
+
+    /**
+     * Invoked when an instance of the remote inference service is disconnected.
+     */
+    public abstract void onInferenceServiceDisconnected();
+
+
     /**
      * Invoked by the {@link OnDeviceIntelligenceService} inorder to send updates to the inference
      * service if there is a state change to be performed.
@@ -391,6 +421,7 @@
      * Request download for feature that is requested and listen to download progress updates. If
      * the download completes successfully, success callback should be populated.
      *
+     * @param callerUid          UID of the caller that initiated this call chain.
      * @param feature            the feature for which files need to be downlaoded.
      *                           process.
      * @param cancellationSignal signal to attach a listener to, and receive cancellation signals
@@ -398,7 +429,7 @@
      * @param downloadCallback   callback to populate download updates for clients to listen on..
      */
     public abstract void onDownloadFeature(
-            @NonNull Feature feature,
+            int callerUid, @NonNull Feature feature,
             @Nullable CancellationSignal cancellationSignal,
             @NonNull DownloadCallback downloadCallback);
 
@@ -407,20 +438,22 @@
      * implementation use the {@link Feature#getFeatureParams()} as a hint to communicate what
      * details the client is looking for.
      *
-     * @param feature               the feature for which status needs to be known.
-     * @param featureStatusCallback callback to populate the resulting feature status.
+     * @param callerUid              UID of the caller that initiated this call chain.
+     * @param feature                the feature for which status needs to be known.
+     * @param featureDetailsCallback callback to populate the resulting feature status.
      */
-    public abstract void onGetFeatureDetails(@NonNull Feature feature,
+    public abstract void onGetFeatureDetails(int callerUid, @NonNull Feature feature,
             @NonNull OutcomeReceiver<FeatureDetails,
-                    OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> featureStatusCallback);
+                    OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> featureDetailsCallback);
 
 
     /**
      * Get feature using the provided identifier to the remote implementation.
      *
+     * @param callerUid       UID of the caller that initiated this call chain.
      * @param featureCallback callback to populate the features list.
      */
-    public abstract void onGetFeature(int featureId,
+    public abstract void onGetFeature(int callerUid, int featureId,
             @NonNull OutcomeReceiver<Feature,
                     OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> featureCallback);
 
@@ -428,9 +461,10 @@
      * List all features which are available in the remote implementation. The implementation might
      * choose to provide only a certain list of features based on the caller.
      *
+     * @param callerUid            UID of the caller that initiated this call chain.
      * @param listFeaturesCallback callback to populate the features list.
      */
-    public abstract void onListFeatures(@NonNull OutcomeReceiver<List<Feature>,
+    public abstract void onListFeatures(int callerUid, @NonNull OutcomeReceiver<List<Feature>,
             OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException> listFeaturesCallback);
 
     /**
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceTrustedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
similarity index 73%
rename from core/java/android/service/ondeviceintelligence/OnDeviceTrustedInferenceService.java
rename to core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
index 8600197..7f7f9c2 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceTrustedInferenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
@@ -16,6 +16,7 @@
 
 package android.service.ondeviceintelligence;
 
+import static android.app.ondeviceintelligence.OnDeviceIntelligenceManager.AUGMENT_REQUEST_CONTENT_BUNDLE_KEY;
 import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
 
 import android.annotation.CallbackExecutor;
@@ -23,6 +24,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.app.Service;
 import android.app.ondeviceintelligence.Content;
@@ -30,14 +32,18 @@
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.app.ondeviceintelligence.IResponseCallback;
 import android.app.ondeviceintelligence.IStreamingResponseCallback;
-import android.app.ondeviceintelligence.ITokenCountCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
 import android.app.ondeviceintelligence.OnDeviceIntelligenceManager;
 import android.app.ondeviceintelligence.ProcessingSignal;
-import android.app.ondeviceintelligence.StreamingResponseReceiver;
+import android.app.ondeviceintelligence.ProcessingOutcomeReceiver;
+import android.app.ondeviceintelligence.StreamedProcessingOutcomeReceiver;
+import android.app.ondeviceintelligence.TokenInfo;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
@@ -75,8 +81,8 @@
  *
  * <pre>
  * {@literal
- * <service android:name=".SampleTrustedInferenceService"
- *          android:permission="android.permission.BIND_ONDEVICE_TRUSTED_INFERENCE_SERVICE"
+ * <service android:name=".SampleSandboxedInferenceService"
+ *          android:permission="android.permission.BIND_ONDEVICE_SANDBOXED_INFERENCE_SERVICE"
  *          android:isolatedProcess="true">
  * </service>}
  * </pre>
@@ -85,18 +91,18 @@
  */
 @SystemApi
 @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
-public abstract class OnDeviceTrustedInferenceService extends Service {
-    private static final String TAG = OnDeviceTrustedInferenceService.class.getSimpleName();
+public abstract class OnDeviceSandboxedInferenceService extends Service {
+    private static final String TAG = OnDeviceSandboxedInferenceService.class.getSimpleName();
 
     /**
      * The {@link Intent} that must be declared as handled by the service. To be supported, the
      * service must also require the
-     * {@link android.Manifest.permission#BIND_ON_DEVICE_TRUSTED_INFERENCE_SERVICE}
+     * {@link android.Manifest.permission#BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE}
      * permission so that other applications can not abuse it.
      */
     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
     public static final String SERVICE_INTERFACE =
-            "android.service.ondeviceintelligence.OnDeviceTrustedInferenceService";
+            "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService";
 
     private IRemoteStorageService mRemoteStorageService;
 
@@ -107,7 +113,7 @@
     @Override
     public final IBinder onBind(@NonNull Intent intent) {
         if (SERVICE_INTERFACE.equals(intent.getAction())) {
-            return new IOnDeviceTrustedInferenceService.Stub() {
+            return new IOnDeviceSandboxedInferenceService.Stub() {
                 @Override
                 public void registerRemoteStorageService(IRemoteStorageService storageService) {
                     Objects.requireNonNull(storageService);
@@ -115,50 +121,48 @@
                 }
 
                 @Override
-                public void requestTokenCount(Feature feature, Content request,
+                public void requestTokenInfo(int callerUid, Feature feature, Content request,
                         ICancellationSignal cancellationSignal,
-                        ITokenCountCallback tokenCountCallback) {
+                        ITokenInfoCallback tokenInfoCallback) {
                     Objects.requireNonNull(feature);
-                    Objects.requireNonNull(tokenCountCallback);
-                    OnDeviceTrustedInferenceService.this.onCountTokens(feature,
+                    Objects.requireNonNull(tokenInfoCallback);
+                    OnDeviceSandboxedInferenceService.this.onTokenInfoRequest(callerUid,
+                            feature,
                             request,
                             CancellationSignal.fromTransport(cancellationSignal),
-                            wrapTokenCountCallback(tokenCountCallback));
+                            wrapTokenInfoCallback(tokenInfoCallback));
                 }
 
                 @Override
-                public void processRequestStreaming(Feature feature, Content request,
+                public void processRequestStreaming(int callerUid, Feature feature, Content request,
                         int requestType, ICancellationSignal cancellationSignal,
                         IProcessingSignal processingSignal,
                         IStreamingResponseCallback callback) {
                     Objects.requireNonNull(feature);
-                    Objects.requireNonNull(request);
                     Objects.requireNonNull(callback);
 
-                    OnDeviceTrustedInferenceService.this.onProcessRequestStreaming(feature,
+                    OnDeviceSandboxedInferenceService.this.onProcessRequestStreaming(callerUid,
+                            feature,
                             request,
                             requestType,
                             CancellationSignal.fromTransport(cancellationSignal),
                             ProcessingSignal.fromTransport(processingSignal),
-                            wrapStreamingResponseCallback(callback)
-                    );
+                            wrapStreamingResponseCallback(callback));
                 }
 
                 @Override
-                public void processRequest(Feature feature, Content request,
+                public void processRequest(int callerUid, Feature feature, Content request,
                         int requestType, ICancellationSignal cancellationSignal,
                         IProcessingSignal processingSignal,
                         IResponseCallback callback) {
                     Objects.requireNonNull(feature);
-                    Objects.requireNonNull(request);
                     Objects.requireNonNull(callback);
 
-
-                    OnDeviceTrustedInferenceService.this.onProcessRequest(feature, request,
-                            requestType, CancellationSignal.fromTransport(cancellationSignal),
+                    OnDeviceSandboxedInferenceService.this.onProcessRequest(callerUid, feature,
+                            request, requestType,
+                            CancellationSignal.fromTransport(cancellationSignal),
                             ProcessingSignal.fromTransport(processingSignal),
-                            wrapResponseCallback(callback)
-                    );
+                            wrapResponseCallback(callback));
                 }
 
                 @Override
@@ -167,7 +171,7 @@
                     Objects.requireNonNull(processingState);
                     Objects.requireNonNull(callback);
 
-                    OnDeviceTrustedInferenceService.this.onUpdateProcessingState(processingState,
+                    OnDeviceSandboxedInferenceService.this.onUpdateProcessingState(processingState,
                             wrapOutcomeReceiver(callback)
                     );
                 }
@@ -178,35 +182,37 @@
     }
 
     /**
-     * Invoked when caller  wants to obtain a count of number of tokens present in the passed in
-     * Request associated with the provided feature.
+     * Invoked when caller  wants to obtain token info related to the payload in the passed
+     * content, associated with the provided feature.
      * The expectation from the implementation is that when processing is complete, it
-     * should provide the token count in the {@link OutcomeReceiver#onResult}.
+     * should provide the token info in the {@link OutcomeReceiver#onResult}.
      *
+     * @param callerUid          UID of the caller that initiated this call chain.
      * @param feature            feature which is associated with the request.
      * @param request            request that requires processing.
      * @param cancellationSignal Cancellation Signal to receive cancellation events from client and
      *                           configure a listener to.
-     * @param callback           callback to populate failure and full response for the provided
+     * @param callback           callback to populate failure or the token info for the provided
      *                           request.
      */
     @NonNull
-    public abstract void onCountTokens(
-            @NonNull Feature feature,
+    public abstract void onTokenInfoRequest(
+            int callerUid, @NonNull Feature feature,
             @NonNull Content request,
             @Nullable CancellationSignal cancellationSignal,
-            @NonNull OutcomeReceiver<Long,
+            @NonNull OutcomeReceiver<TokenInfo,
                     OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> callback);
 
     /**
      * Invoked when caller provides a request for a particular feature to be processed in a
      * streaming manner. The expectation from the implementation is that when processing the
      * request,
-     * it periodically populates the {@link StreamingResponseReceiver#onNewContent} to continuously
+     * it periodically populates the {@link StreamedProcessingOutcomeReceiver#onNewContent} to continuously
      * provide partial Content results for the caller to utilize. Optionally the implementation can
-     * provide the complete response in the {@link StreamingResponseReceiver#onResult} upon
+     * provide the complete response in the {@link StreamedProcessingOutcomeReceiver#onResult} upon
      * processing completion.
      *
+     * @param callerUid          UID of the caller that initiated this call chain.
      * @param feature            feature which is associated with the request.
      * @param request            request that requires processing.
      * @param requestType        identifier representing the type of request.
@@ -218,13 +224,12 @@
      */
     @NonNull
     public abstract void onProcessRequestStreaming(
-            @NonNull Feature feature,
-            @NonNull Content request,
+            int callerUid, @NonNull Feature feature,
+            @Nullable Content request,
             @OnDeviceIntelligenceManager.RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
-            @NonNull StreamingResponseReceiver<Content, Content,
-                    OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> callback);
+            @NonNull StreamedProcessingOutcomeReceiver callback);
 
     /**
      * Invoked when caller provides a request for a particular feature to be processed in one shot
@@ -233,6 +238,7 @@
      * should
      * provide the complete response in the {@link OutcomeReceiver#onResult}.
      *
+     * @param callerUid          UID of the caller that initiated this call chain.
      * @param feature            feature which is associated with the request.
      * @param request            request that requires processing.
      * @param requestType        identifier representing the type of request.
@@ -244,13 +250,12 @@
      */
     @NonNull
     public abstract void onProcessRequest(
-            @NonNull Feature feature,
-            @NonNull Content request,
+            int callerUid, @NonNull Feature feature,
+            @Nullable Content request,
             @OnDeviceIntelligenceManager.RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
-            @NonNull OutcomeReceiver<Content,
-                    OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> callback);
+            @NonNull ProcessingOutcomeReceiver callback);
 
 
     /**
@@ -335,6 +340,26 @@
         }
     }
 
+
+    /**
+     * Returns the {@link Executor} to use for incoming IPC from request sender into your service
+     * implementation. For e.g. see
+     * {@link ProcessingOutcomeReceiver#onDataAugmentRequest(Content,
+     * Consumer)} where we use the executor to populate the consumer.
+     * <p>
+     * Override this method in your {@link OnDeviceSandboxedInferenceService} implementation to
+     * provide the executor you want to use for incoming IPC.
+     *
+     * @return the {@link Executor} to use for incoming IPC from {@link OnDeviceIntelligenceManager}
+     * to {@link OnDeviceSandboxedInferenceService}.
+     */
+    @SuppressLint("OnNameExpected")
+    @NonNull
+    public Executor getCallbackExecutor() {
+        return new HandlerExecutor(Handler.createAsync(getMainLooper()));
+    }
+
+
     private RemoteCallback wrapResultReceiverAsReadOnly(
             @NonNull Consumer<Map<String, FileInputStream>> resultConsumer,
             @NonNull Executor executor) {
@@ -355,10 +380,9 @@
         });
     }
 
-    private OutcomeReceiver<Content,
-            OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapResponseCallback(
+    private ProcessingOutcomeReceiver wrapResponseCallback(
             IResponseCallback callback) {
-        return new OutcomeReceiver<>() {
+        return new ProcessingOutcomeReceiver() {
             @Override
             public void onResult(@androidx.annotation.NonNull Content response) {
                 try {
@@ -378,13 +402,23 @@
                     Slog.e(TAG, "Error sending result: " + e);
                 }
             }
+
+            @Override
+            public void onDataAugmentRequest(@NonNull Content content,
+                    @NonNull Consumer<Content> contentCallback) {
+                try {
+                    callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback));
+
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending augment request: " + e);
+                }
+            }
         };
     }
 
-    private StreamingResponseReceiver<Content, Content,
-            OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapStreamingResponseCallback(
+    private StreamedProcessingOutcomeReceiver wrapStreamingResponseCallback(
             IStreamingResponseCallback callback) {
-        return new StreamingResponseReceiver<>() {
+        return new StreamedProcessingOutcomeReceiver() {
             @Override
             public void onNewContent(@androidx.annotation.NonNull Content content) {
                 try {
@@ -413,17 +447,43 @@
                     Slog.e(TAG, "Error sending result: " + e);
                 }
             }
+
+            @Override
+            public void onDataAugmentRequest(@NonNull Content content,
+                    @NonNull Consumer<Content> contentCallback) {
+                try {
+                    callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback));
+
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending augment request: " + e);
+                }
+            }
         };
     }
 
-    private OutcomeReceiver<Long,
-            OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapTokenCountCallback(
-            ITokenCountCallback tokenCountCallback) {
+    private RemoteCallback wrapRemoteCallback(
+            @androidx.annotation.NonNull Consumer<Content> contentCallback) {
+        return new RemoteCallback(
+                result -> {
+                    if (result != null) {
+                        getCallbackExecutor().execute(() -> contentCallback.accept(
+                                result.getParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY,
+                                        Content.class)));
+                    } else {
+                        getCallbackExecutor().execute(
+                                () -> contentCallback.accept(null));
+                    }
+                });
+    }
+
+    private OutcomeReceiver<TokenInfo,
+            OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException> wrapTokenInfoCallback(
+            ITokenInfoCallback tokenInfoCallback) {
         return new OutcomeReceiver<>() {
             @Override
-            public void onResult(Long tokenCount) {
+            public void onResult(TokenInfo tokenInfo) {
                 try {
-                    tokenCountCallback.onSuccess(tokenCount);
+                    tokenInfoCallback.onSuccess(tokenInfo);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error sending result: " + e);
                 }
@@ -433,7 +493,7 @@
             public void onError(
                     OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerProcessingException exception) {
                 try {
-                    tokenCountCallback.onFailure(exception.getErrorCode(), exception.getMessage(),
+                    tokenInfoCallback.onFailure(exception.getErrorCode(), exception.getMessage(),
                             exception.getErrorParams());
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error sending failure: " + e);
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 67a3978..0ae3e59 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -1775,9 +1775,9 @@
     @CriticalNative
     private static native void nativeOffsetLocation(long nativePtr, float deltaX, float deltaY);
     @CriticalNative
-    private static native float nativeGetXOffset(long nativePtr);
+    private static native float nativeGetRawXOffset(long nativePtr);
     @CriticalNative
-    private static native float nativeGetYOffset(long nativePtr);
+    private static native float nativeGetRawYOffset(long nativePtr);
     @CriticalNative
     private static native float nativeGetXPrecision(long nativePtr);
     @CriticalNative
@@ -3745,7 +3745,7 @@
                     nativeGetAction(mNativePtr), nativeGetFlags(mNativePtr),
                     nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr),
                     nativeGetButtonState(mNativePtr), nativeGetClassification(mNativePtr),
-                    nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr),
+                    nativeGetRawXOffset(mNativePtr), nativeGetRawYOffset(mNativePtr),
                     nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr),
                     nativeGetDownTimeNanos(mNativePtr),
                     nativeGetEventTimeNanos(mNativePtr, HISTORY_CURRENT),
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index d29963c..828004b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -25,7 +25,6 @@
 import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
 import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
 import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
-import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
@@ -5630,7 +5629,7 @@
     @Nullable
     private ViewTranslationCallback mViewTranslationCallback;
 
-    private float mFrameContentVelocity = -1;
+    private float mFrameContentVelocity = 0;
 
     @Nullable
 
@@ -5661,9 +5660,6 @@
     protected long mMinusTwoFrameIntervalMillis = 0;
     private int mLastFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
 
-    private float mLastFrameX = Float.NaN;
-    private float mLastFrameY = Float.NaN;
-
     @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = Float.NaN;
     @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
@@ -24601,10 +24597,7 @@
     public void draw(@NonNull Canvas canvas) {
         final int privateFlags = mPrivateFlags;
         mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
-
-        mFrameContentVelocity = -1;
-        mLastFrameX = mLeft + mRenderNode.getTranslationX();
-        mLastFrameY = mTop + mRenderNode.getTranslationY();
+        mFrameContentVelocity = 0;
 
         /*
          * Draw traversal performs several drawing steps which must be executed
@@ -33680,17 +33673,6 @@
             if (sToolkitMetricsForFrameRateDecisionFlagValue) {
                 viewRootImpl.recordViewPercentage(sizePercentage);
             }
-            if (viewVelocityApi()) {
-                float velocity = mFrameContentVelocity;
-                if (velocity < 0f) {
-                    velocity = calculateVelocity();
-                }
-                if (velocity > 0f) {
-                    float frameRate = convertVelocityToFrameRate(velocity);
-                    viewRootImpl.votePreferredFrameRate(frameRate, FRAME_RATE_COMPATIBILITY_GTE);
-                    return;
-                }
-            }
             if (!Float.isNaN(mPreferredFrameRate)) {
                 if (mPreferredFrameRate < 0) {
                     if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) {
@@ -33713,23 +33695,6 @@
         }
     }
 
-    private float convertVelocityToFrameRate(float velocityPps) {
-        float density = getResources().getDisplayMetrics().density;
-        float velocityDps = velocityPps / density;
-        // Choose a frame rate in increments of 10fps
-        return Math.min(140f, 60f + (10f * (float) Math.floor(velocityDps / 300f)));
-    }
-
-    private float calculateVelocity() {
-        // This current calculation is very simple. If something on the screen moved, then
-        // it votes for the highest velocity. If it doesn't move, then return 0.
-        float x = mLeft + mRenderNode.getTranslationX();
-        float y = mTop + mRenderNode.getTranslationY();
-
-        return (!Float.isNaN(mLastFrameX) && (x != mLastFrameX || y != mLastFrameY))
-                ? 100_000f : 0f;
-    }
-
     /**
      * Set the current velocity of the View, we only track positive value.
      * We will use the velocity information to adjust the frame rate when applicable.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 42f6405..02f8e6e 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -31,7 +31,6 @@
 import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
 import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
 import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
-import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE;
 import static android.view.View.PFLAG_DRAW_ANIMATION;
 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
@@ -7572,8 +7571,7 @@
             }
 
             // For the variable refresh rate project
-            if (handled && shouldTouchBoost(action & MotionEvent.ACTION_MASK,
-                    mWindowAttributes.type)) {
+            if (handled && shouldTouchBoost(action, mWindowAttributes.type)) {
                 // set the frame rate to the maximum value.
                 mIsTouchBoosting = true;
                 setPreferredFrameRateCategory(mPreferredFrameRateCategory);
@@ -12400,17 +12398,6 @@
                     mFrameRateCompatibility).applyAsyncUnsafe();
                 mLastPreferredFrameRate = preferredFrameRate;
             }
-            if (mFrameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE && mIsTouchBoosting) {
-                // We've received a velocity, so we'll let the velocity control the
-                // frame rate unless we receive additional motion events.
-                mIsTouchBoosting = false;
-                if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
-                    Trace.instant(
-                            Trace.TRACE_TAG_VIEW,
-                            "ViewRootImpl#setFrameRate velocity used, no touch boost on next frame"
-                    );
-                }
-            }
         } catch (Exception e) {
             Log.e(mTag, "Unable to set frame rate", e);
         } finally {
@@ -12436,8 +12423,9 @@
     }
 
     private boolean shouldTouchBoost(int motionEventAction, int windowType) {
-        // boost for almost all input
-        boolean desiredAction = motionEventAction != MotionEvent.ACTION_OUTSIDE;
+        boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN
+                || motionEventAction == MotionEvent.ACTION_MOVE
+                || motionEventAction == MotionEvent.ACTION_UP;
         boolean undesiredType = windowType == TYPE_INPUT_METHOD
                 && sToolkitFrameRateTypingReadOnlyFlagValue;
         // use toolkitSetFrameRate flag to gate the change
@@ -12543,14 +12531,6 @@
     }
 
     /**
-     * Returns whether touch boost is currently enabled.
-     */
-    @VisibleForTesting
-    public boolean getIsTouchBoosting() {
-        return mIsTouchBoosting;
-    }
-
-    /**
      * Get the value of mFrameRateCompatibility
      */
     @VisibleForTesting
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index fe4ac4e..a2d8d80 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -4299,7 +4299,7 @@
         }
 
         if (mode == MODE_NORMAL) {
-            mApplication = ApplicationInfo.CREATOR.createFromParcel(parcel);
+            mApplication = parcel.readTypedObject(ApplicationInfo.CREATOR);
             mIdealSize = parcel.readInt() == 0 ? null : SizeF.CREATOR.createFromParcel(parcel);
             mLayoutId = parcel.readInt();
             mViewId = parcel.readInt();
@@ -6805,7 +6805,7 @@
                 mBitmapCache.writeBitmapsToParcel(dest, flags);
                 mCollectionCache.writeToParcel(dest, flags, intentsToIgnore);
             }
-            mApplication.writeToParcel(dest, flags);
+            dest.writeTypedObject(mApplication, flags);
             if (mIsRoot || mIdealSize == null) {
                 dest.writeInt(0);
             } else {
@@ -6893,7 +6893,8 @@
      * @hide
      */
     public boolean hasSameAppInfo(ApplicationInfo info) {
-        return mApplication.packageName.equals(info.packageName) && mApplication.uid == info.uid;
+        return mApplication == null || mApplication.packageName.equals(info.packageName)
+                && mApplication.uid == info.uid;
     }
 
     /**
@@ -7672,8 +7673,7 @@
             byte[] instruction;
             final List<byte[]> instructions = new ArrayList<>(size);
             for (int i = 0; i < size; i++) {
-                instruction = new byte[in.readInt()];
-                in.readByteArray(instruction);
+                instruction = in.readBlob();
                 instructions.add(instruction);
             }
             return new DrawInstructions(instructions);
@@ -7688,8 +7688,7 @@
             final List<byte[]> instructions = drawInstructions.mInstructions;
             dest.writeInt(instructions.size());
             for (byte[] instruction : instructions) {
-                dest.writeInt(instruction.length);
-                dest.writeByteArray(instruction);
+                dest.writeBlob(instruction);
             }
         }
 
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
index 30de546..2096ba4 100644
--- a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
@@ -55,37 +55,38 @@
  * A service for the ProtoLog logging system.
  */
 public class LegacyProtoLogImpl implements IProtoLog {
-    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
-
     private static final int BUFFER_CAPACITY = 1024 * 1024;
     private static final int PER_CHUNK_SIZE = 1024;
     private static final String TAG = "ProtoLog";
     private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
     static final String PROTOLOG_VERSION = "2.0.0";
-    private static final int DEFAULT_PER_CHUNK_SIZE = 0;
 
     private final File mLogFile;
     private final String mLegacyViewerConfigFilename;
     private final TraceBuffer mBuffer;
     private final LegacyProtoLogViewerConfigReader mViewerConfig;
+    private final TreeMap<String, IProtoLogGroup> mLogGroups;
     private final int mPerChunkSize;
 
     private boolean mProtoLogEnabled;
     private boolean mProtoLogEnabledLockFree;
     private final Object mProtoLogEnabledLock = new Object();
 
-    public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename) {
+    public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename,
+            TreeMap<String, IProtoLogGroup> logGroups) {
         this(new File(outputFile), viewerConfigFilename, BUFFER_CAPACITY,
-                new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE);
+                new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE, logGroups);
     }
 
     public LegacyProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
-            LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize) {
+            LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize,
+            TreeMap<String, IProtoLogGroup> logGroups) {
         mLogFile = file;
         mBuffer = new TraceBuffer(bufferCapacity);
         mLegacyViewerConfigFilename = viewerConfigFilename;
         mViewerConfig = viewerConfig;
         mPerChunkSize = perChunkSize;
+        this.mLogGroups = logGroups;
     }
 
     /**
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 53062d8..bdd9a91 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -22,9 +22,9 @@
 import static perfetto.protos.PerfettoTrace.InternedString.STR;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.BOOLEAN_PARAMS;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.DOUBLE_PARAMS;
-import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STACKTRACE_IID;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.MESSAGE_ID;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.SINT64_PARAMS;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STACKTRACE_IID;
 import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STR_PARAM_IIDS;
 import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.GROUPS;
 import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.Group.ID;
@@ -78,7 +78,6 @@
  * A service for the ProtoLog logging system.
  */
 public class PerfettoProtoLogImpl implements IProtoLog {
-    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
     private static final String LOG_TAG = "ProtoLog";
     private final AtomicInteger mTracingInstances = new AtomicInteger();
 
@@ -89,8 +88,10 @@
     );
     private final ProtoLogViewerConfigReader mViewerConfigReader;
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+    private final TreeMap<String, IProtoLogGroup> mLogGroups;
 
-    public PerfettoProtoLogImpl(String viewerConfigFilePath) {
+    public PerfettoProtoLogImpl(String viewerConfigFilePath,
+            TreeMap<String, IProtoLogGroup> logGroups) {
         this(() -> {
             try {
                 return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
@@ -98,23 +99,28 @@
                 Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e);
                 return null;
             }
-        });
+        }, logGroups);
     }
 
-    public PerfettoProtoLogImpl(ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
+    public PerfettoProtoLogImpl(
+            ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            TreeMap<String, IProtoLogGroup> logGroups
+    ) {
         this(viewerConfigInputStreamProvider,
-                new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
+                new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider), logGroups);
     }
 
     @VisibleForTesting
     public PerfettoProtoLogImpl(
             ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            ProtoLogViewerConfigReader viewerConfigReader
+            ProtoLogViewerConfigReader viewerConfigReader,
+            TreeMap<String, IProtoLogGroup> logGroups
     ) {
         Producer.init(InitArguments.DEFAULTS);
         mDataSource.register(DataSourceParams.DEFAULTS);
         this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
         this.mViewerConfigReader = viewerConfigReader;
+        this.mLogGroups = logGroups;
     }
 
     /**
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 8965385..487ae814 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH;
 import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH;
+import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LOG_GROUPS;
 import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH;
 
 import android.annotation.Nullable;
@@ -28,6 +29,8 @@
 import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.protolog.common.ProtoLogToolInjected;
 
+import java.util.TreeMap;
+
 /**
  * A service for the ProtoLog logging system.
  */
@@ -43,6 +46,9 @@
     @ProtoLogToolInjected(LEGACY_OUTPUT_FILE_PATH)
     private static String sLegacyOutputFilePath;
 
+    @ProtoLogToolInjected(LOG_GROUPS)
+    private static TreeMap<String, IProtoLogGroup> sLogGroups;
+
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
     public static void d(IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString,
@@ -99,11 +105,10 @@
     public static synchronized IProtoLog getSingleInstance() {
         if (sServiceInstance == null) {
             if (android.tracing.Flags.perfettoProtologTracing()) {
-                sServiceInstance =
-                        new PerfettoProtoLogImpl(sViewerConfigPath);
+                sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sLogGroups);
             } else {
-                sServiceInstance =
-                        new LegacyProtoLogImpl(sLegacyOutputFilePath, sLegacyViewerConfigPath);
+                sServiceInstance = new LegacyProtoLogImpl(
+                        sLegacyOutputFilePath, sLegacyViewerConfigPath, sLogGroups);
             }
         }
         return sServiceInstance;
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index 3c206ac..ae3d448 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -4,6 +4,7 @@
 import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE;
 import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
 
+import android.util.ArrayMap;
 import android.util.proto.ProtoInputStream;
 
 import com.android.internal.protolog.common.ILogger;
@@ -57,6 +58,7 @@
     }
 
     private void doLoadViewerConfig() throws IOException {
+        mLogMessageMap = new ArrayMap<>();
         final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
 
         while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
diff --git a/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
index ffd0d76..17c82d7 100644
--- a/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
+++ b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
@@ -22,7 +22,9 @@
 
 @Target({ElementType.FIELD, ElementType.PARAMETER})
 public @interface ProtoLogToolInjected {
-    enum Value { VIEWER_CONFIG_PATH, LEGACY_OUTPUT_FILE_PATH, LEGACY_VIEWER_CONFIG_PATH }
+    enum Value {
+        VIEWER_CONFIG_PATH, LEGACY_OUTPUT_FILE_PATH, LEGACY_VIEWER_CONFIG_PATH, LOG_GROUPS
+    }
 
     Value value();
 }
diff --git a/core/jni/LayoutlibLoader.cpp b/core/jni/LayoutlibLoader.cpp
index 071f933..83b6afa 100644
--- a/core/jni/LayoutlibLoader.cpp
+++ b/core/jni/LayoutlibLoader.cpp
@@ -428,7 +428,7 @@
         return JNI_ERR;
     }
 
-    if (!systemProperties["register_properties_during_load"].empty()) {
+    if (!systemProperties["icu.data.path"].empty()) {
         // Set the location of ICU data
         bool icuInitialized = init_icu(systemProperties["icu.data.path"].c_str());
         if (!icuInitialized) {
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index 23adb8f7..1a86363 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -411,8 +411,8 @@
             jniThrowNullPointerException(env, "pointerCoords");
             return;
         }
-        pointerCoordsToNative(env, pointerCoordsObj,
-                event->getXOffset(), event->getYOffset(), &rawPointerCoords[i]);
+        pointerCoordsToNative(env, pointerCoordsObj, event->getRawXOffset(), event->getRawYOffset(),
+                              &rawPointerCoords[i]);
         env->DeleteLocalRef(pointerCoordsObj);
     }
 
@@ -735,14 +735,14 @@
     return event->offsetLocation(deltaX, deltaY);
 }
 
-static jfloat android_view_MotionEvent_nativeGetXOffset(jlong nativePtr) {
+static jfloat android_view_MotionEvent_nativeGetRawXOffset(jlong nativePtr) {
     MotionEvent* event = reinterpret_cast<MotionEvent*>(nativePtr);
-    return event->getXOffset();
+    return event->getRawXOffset();
 }
 
-static jfloat android_view_MotionEvent_nativeGetYOffset(jlong nativePtr) {
+static jfloat android_view_MotionEvent_nativeGetRawYOffset(jlong nativePtr) {
     MotionEvent* event = reinterpret_cast<MotionEvent*>(nativePtr);
-    return event->getYOffset();
+    return event->getRawYOffset();
 }
 
 static jfloat android_view_MotionEvent_nativeGetXPrecision(jlong nativePtr) {
@@ -871,8 +871,8 @@
         {"nativeGetClassification", "(J)I",
          (void*)android_view_MotionEvent_nativeGetClassification},
         {"nativeOffsetLocation", "(JFF)V", (void*)android_view_MotionEvent_nativeOffsetLocation},
-        {"nativeGetXOffset", "(J)F", (void*)android_view_MotionEvent_nativeGetXOffset},
-        {"nativeGetYOffset", "(J)F", (void*)android_view_MotionEvent_nativeGetYOffset},
+        {"nativeGetRawXOffset", "(J)F", (void*)android_view_MotionEvent_nativeGetRawXOffset},
+        {"nativeGetRawYOffset", "(J)F", (void*)android_view_MotionEvent_nativeGetRawYOffset},
         {"nativeGetXPrecision", "(J)F", (void*)android_view_MotionEvent_nativeGetXPrecision},
         {"nativeGetYPrecision", "(J)F", (void*)android_view_MotionEvent_nativeGetYPrecision},
         {"nativeGetXCursorPosition", "(J)F",
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 52bad21..9d80d153 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7955,12 +7955,12 @@
         android:protectionLevel="signature|privileged" />
 
 
-    <!-- @SystemApi Allows an app to bind the on-device trusted service.
+    <!-- @SystemApi Allows an app to bind the on-device sandboxed service.
              <p>Protection level: signature|privileged
              @hide
          @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence")
         -->
-    <permission android:name="android.permission.BIND_ON_DEVICE_TRUSTED_SERVICE"
+    <permission android:name="android.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE"
         android:protectionLevel="signature"/>
 
 
diff --git a/core/res/res/values-watch/themes_device_defaults.xml b/core/res/res/values-watch/themes_device_defaults.xml
index df8158d..6e804c0 100644
--- a/core/res/res/values-watch/themes_device_defaults.xml
+++ b/core/res/res/values-watch/themes_device_defaults.xml
@@ -146,7 +146,7 @@
         <item name="windowAnimationStyle">@style/Animation.InputMethod</item>
         <item name="imeFullscreenBackground">?colorBackground</item>
         <item name="imeExtractEnterAnimation">@anim/input_method_extract_enter</item>
-        <item name="windowSwipeToDismiss">false</item>
+        <item name="windowSwipeToDismiss">true</item>
     </style>
 
     <!-- DeviceDefault theme for dialog windows and activities. In contrast to Material, the
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 4f20fce..9d902c9 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4673,8 +4673,8 @@
     <!-- The component name for the default system on-device intelligence service, -->
     <string name="config_defaultOnDeviceIntelligenceService" translatable="false"></string>
 
-    <!-- The component name for the default system on-device trusted inference service. -->
-    <string name="config_defaultOnDeviceTrustedInferenceService" translatable="false"></string>
+    <!-- The component name for the default system on-device sandboxed inference service. -->
+    <string name="config_defaultOnDeviceSandboxedInferenceService" translatable="false"></string>
 
     <!-- Component name that accepts ACTION_SEND intents for requesting ambient context consent for
          wearable sensing. -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 59066eb..238772f 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6434,4 +6434,6 @@
     <string name="satellite_notification_open_message">Open Messages</string>
     <!-- Invoke Satellite setting activity of Settings -->
     <string name="satellite_notification_how_it_works">How it works</string>
+    <!-- Initial/System provided label shown for an app which gets unarchived. [CHAR LIMIT=64]. -->
+    <string name="unarchival_session_app_label">Pending...</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 150951f..4b71654 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3916,7 +3916,7 @@
   <java-symbol type="string" name="config_ambientContextEventArrayExtraKey" />
   <java-symbol type="string" name="config_defaultWearableSensingService" />
   <java-symbol type="string" name="config_defaultOnDeviceIntelligenceService" />
-  <java-symbol type="string" name="config_defaultOnDeviceTrustedInferenceService" />
+  <java-symbol type="string" name="config_defaultOnDeviceSandboxedInferenceService" />
   <java-symbol type="string" name="config_retailDemoPackage" />
   <java-symbol type="string" name="config_retailDemoPackageSignature" />
 
@@ -5373,4 +5373,6 @@
   <java-symbol type="string" name="config_defaultContextualSearchKey" />
   <java-symbol type="string" name="config_defaultContextualSearchEnabled" />
   <java-symbol type="string" name="config_defaultContextualSearchLegacyEnabled" />
+
+  <java-symbol type="string" name="unarchival_session_app_label" />
 </resources>
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
index 15bb66b..8d9fad9 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
@@ -90,7 +90,7 @@
     private static final long TEST_HD_FREQUENCY_VALUE = 95_300;
     private static final long TEST_HD_STATION_ID_EXT_VALUE = 0x100000001L
             | (TEST_HD_FREQUENCY_VALUE << 36);
-    private static final long TEST_HD_LOCATION_VALUE = 0x89CC8E06CCB9ECL;
+    private static final long TEST_HD_LOCATION_VALUE =  0x4E647007665CF6L;
     private static final long TEST_VENDOR_ID_VALUE = 9_901;
 
     private static final ProgramSelector.Identifier TEST_DAB_SID_EXT_ID =
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index 37a499a..6cc5485 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -110,8 +110,6 @@
             Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
             Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
             Paths.get("/data/misc/wmtrace/wm_log.winscope"),
-            Paths.get("/data/misc/wmtrace/wm_transition_trace.winscope"),
-            Paths.get("/data/misc/wmtrace/shell_transition_trace.winscope"),
     };
 
     private Handler mHandler;
@@ -257,6 +255,38 @@
         assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
     }
 
+    @LargeTest
+    @Test
+    public void noPreDumpData_then_fullWithUsePreDumpFlag_ignoresFlag() throws Exception {
+        startPreDumpedUiTraces();
+
+        mBrm.preDumpUiData();
+        waitTillDumpstateExitedOrTimeout();
+
+        // Simulate lost of pre-dumped data.
+        // For example it can happen in this scenario:
+        // 1. Pre-dump data
+        // 2. Start bugreport + "use pre-dump" flag (USE AND REMOVE THE PRE-DUMP FROM DISK)
+        // 3. Start bugreport + "use pre-dump" flag (NO PRE-DUMP AVAILABLE ON DISK)
+        removeFilesIfNeeded(UI_TRACES_PREDUMPED);
+
+        // Start bugreport with "use predump" flag. Because the pre-dumped data is not available
+        // the flag will be ignored and data will be dumped as in normal flow.
+        BugreportCallbackImpl callback = new BugreportCallbackImpl();
+        mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
+                callback);
+        shareConsentDialog(ConsentReply.ALLOW);
+        waitTillDoneOrTimeout(callback);
+
+        stopPreDumpedUiTraces();
+
+        assertThat(callback.isDone()).isTrue();
+        assertThat(mBugreportFile.length()).isGreaterThan(0L);
+        assertFdsAreClosed(mBugreportFd);
+
+        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+    }
+
     @Test
     public void simultaneousBugreportsNotAllowed() throws Exception {
         // Start bugreport #1
@@ -506,9 +536,6 @@
         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
                 "cmd window tracing start"
         );
-        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
-                "service call SurfaceFlinger 1025 i32 1"
-        );
     }
 
     private static void stopPreDumpedUiTraces() {
@@ -611,6 +638,14 @@
         return files;
     }
 
+    private static void removeFilesIfNeeded(Path[] paths) throws Exception {
+        for (Path path : paths) {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                    "rm -f " + path.toString()
+            );
+        }
+    }
+
     private static ParcelFileDescriptor parcelFd(File file) throws Exception {
         return ParcelFileDescriptor.open(file,
                 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
diff --git a/core/tests/coretests/res/layout/view_velocity_test.xml b/core/tests/coretests/res/layout/view_velocity_test.xml
deleted file mode 100644
index 98154a4..0000000
--- a/core/tests/coretests/res/layout/view_velocity_test.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2024 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/frameLayout"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <View
-        android:id="@+id/moving_view"
-        android:layout_width="50dp"
-        android:layout_height="50dp" />
-</FrameLayout>
diff --git a/core/tests/coretests/src/android/view/ViewVelocityTest.java b/core/tests/coretests/src/android/view/ViewVelocityTest.java
deleted file mode 100644
index 128c54b..0000000
--- a/core/tests/coretests/src/android/view/ViewVelocityTest.java
+++ /dev/null
@@ -1,102 +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 android.view;
-
-import static junit.framework.Assert.assertEquals;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.app.Activity;
-import android.os.SystemClock;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.filters.SmallTest;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.frameworks.coretests.R;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ViewVelocityTest {
-
-    @Rule
-    public ActivityTestRule<ViewCaptureTestActivity> mActivityRule = new ActivityTestRule<>(
-            ViewCaptureTestActivity.class);
-
-    private Activity mActivity;
-    private View mMovingView;
-    private ViewRootImpl mViewRoot;
-
-    @Before
-    public void setUp() throws Throwable {
-        mActivity = mActivityRule.getActivity();
-        mActivityRule.runOnUiThread(() -> {
-            mActivity.setContentView(R.layout.view_velocity_test);
-            mMovingView = mActivity.findViewById(R.id.moving_view);
-        });
-        ViewParent parent = mActivity.getWindow().getDecorView().getParent();
-        while (parent instanceof View) {
-            parent = parent.getParent();
-        }
-        mViewRoot = (ViewRootImpl) parent;
-    }
-
-    @UiThreadTest
-    @Test
-    public void frameRateChangesWhenContentMoves() {
-        mMovingView.offsetLeftAndRight(100);
-        float frameRate = mViewRoot.getPreferredFrameRate();
-        assertTrue(frameRate > 0);
-    }
-
-    @UiThreadTest
-    @Test
-    public void firstFrameNoMovement() {
-        assertEquals(0f, mViewRoot.getPreferredFrameRate(), 0f);
-    }
-
-    @Test
-    public void touchBoostDisable() throws Throwable {
-        mActivityRule.runOnUiThread(() -> {
-            long now = SystemClock.uptimeMillis();
-            MotionEvent down = MotionEvent.obtain(
-                    /* downTime */ now,
-                    /* eventTime */ now,
-                    /* action */ MotionEvent.ACTION_DOWN,
-                    /* x */ 0f,
-                    /* y */ 0f,
-                    /* metaState */ 0
-            );
-            mActivity.dispatchTouchEvent(down);
-            mMovingView.offsetLeftAndRight(10);
-        });
-        mActivityRule.runOnUiThread(() -> {
-            mMovingView.invalidate();
-        });
-
-        mActivityRule.runOnUiThread(() -> {
-            assertFalse(mViewRoot.getIsTouchBoosting());
-        });
-    }
-}
diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java
index b6ce9b6..f9ac02a 100644
--- a/graphics/java/android/graphics/Canvas.java
+++ b/graphics/java/android/graphics/Canvas.java
@@ -789,10 +789,8 @@
      * @param m The 4x4 matrix to preconcatenate with the current matrix
      */
     @FlaggedApi(Flags.FLAG_MATRIX_44)
-    public void concat44(@Nullable Matrix44 m) {
-        if (m != null) {
-            nConcat(mNativeCanvasWrapper, m.mBackingArray);
-        }
+    public void concat(@Nullable Matrix44 m) {
+        if (m != null) nConcat(mNativeCanvasWrapper, m.mBackingArray);
     }
 
     /**
diff --git a/graphics/java/android/graphics/Matrix44.java b/graphics/java/android/graphics/Matrix44.java
index 7cc0eb7..a99e201 100644
--- a/graphics/java/android/graphics/Matrix44.java
+++ b/graphics/java/android/graphics/Matrix44.java
@@ -17,6 +17,7 @@
 package android.graphics;
 
 import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 
 import com.android.graphics.hwui.flags.Flags;
@@ -98,11 +99,11 @@
     /**
      * Gets the value at the matrix's row and column.
      *
-     * @param row An integer from 0 to 4 indicating the row of the value to get
-     * @param col An integer from 0 to 4 indicating the column of the value to get
+     * @param row An integer from 0 to 3 indicating the row of the value to get
+     * @param col An integer from 0 to 3 indicating the column of the value to get
      */
     @FlaggedApi(Flags.FLAG_MATRIX_44)
-    public float get(int row, int col) {
+    public float get(@IntRange(from = 0, to = 3) int row, @IntRange(from = 0, to = 3) int col) {
         if (row >= 0 && row < 4 && col >= 0 && col < 4) {
             return mBackingArray[row * 4 + col];
         }
@@ -112,12 +113,13 @@
     /**
      * Sets the value at the matrix's row and column to the provided value.
      *
-     * @param row An integer from 0 to 4 indicating the row of the value to change
-     * @param col An integer from 0 to 4 indicating the column of the value to change
+     * @param row An integer from 0 to 3 indicating the row of the value to change
+     * @param col An integer from 0 to 3 indicating the column of the value to change
      * @param val The value the element at the specified index will be set to
      */
     @FlaggedApi(Flags.FLAG_MATRIX_44)
-    public void set(int row, int col, float val) {
+    public void set(@IntRange(from = 0, to = 3) int row, @IntRange(from = 0, to = 3) int col,
+            float val) {
         if (row >= 0 && row < 4 && col >= 0 && col < 4) {
             mBackingArray[row * 4 + col] = val;
         } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 4d47ca9..139cde2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -983,7 +983,13 @@
         // cache current min/max size
         Point minSize = mPipBoundsState.getMinSize();
         Point maxSize = mPipBoundsState.getMaxSize();
-        mPipBoundsState.updateMinMaxSize(pictureInPictureParams.getAspectRatioFloat());
+        final float aspectRatioFloat;
+        if (pictureInPictureParams.hasSetAspectRatio()) {
+            aspectRatioFloat = pictureInPictureParams.getAspectRatioFloat();
+        } else {
+            aspectRatioFloat = mPipBoundsAlgorithm.getDefaultAspectRatio();
+        }
+        mPipBoundsState.updateMinMaxSize(aspectRatioFloat);
         final Rect entryBounds = mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo,
                 pictureInPictureParams);
         // restore min/max size, as this is referenced later in OnDisplayChangingListener and needs
diff --git a/location/java/android/location/provider/ForwardGeocodeRequest.java b/location/java/android/location/provider/ForwardGeocodeRequest.java
index 8f227b1..89d14fb 100644
--- a/location/java/android/location/provider/ForwardGeocodeRequest.java
+++ b/location/java/android/location/provider/ForwardGeocodeRequest.java
@@ -260,7 +260,11 @@
             mCallingAttributionTag = null;
         }
 
-        /** Sets the attribution tag. */
+        /**
+         * Sets the attribution tag.
+         *
+         * @param attributionTag The attribution tag to associate with the request.
+         */
         @NonNull
         public Builder setCallingAttributionTag(@NonNull String attributionTag) {
             mCallingAttributionTag = attributionTag;
diff --git a/location/java/android/location/provider/ReverseGeocodeRequest.java b/location/java/android/location/provider/ReverseGeocodeRequest.java
index 57c9047..2107707 100644
--- a/location/java/android/location/provider/ReverseGeocodeRequest.java
+++ b/location/java/android/location/provider/ReverseGeocodeRequest.java
@@ -207,7 +207,11 @@
             mCallingAttributionTag = null;
         }
 
-        /** Sets the attribution tag. */
+        /**
+         * Sets the attribution tag.
+         *
+         * @param attributionTag The attribution tag to associate with the request.
+         */
         @NonNull
         public Builder setCallingAttributionTag(@NonNull String attributionTag) {
             mCallingAttributionTag = attributionTag;
diff --git a/native/android/input.cpp b/native/android/input.cpp
index 53699bc..0a22314 100644
--- a/native/android/input.cpp
+++ b/native/android/input.cpp
@@ -124,11 +124,11 @@
 }
 
 float AMotionEvent_getXOffset(const AInputEvent* motion_event) {
-    return static_cast<const MotionEvent*>(motion_event)->getXOffset();
+    return static_cast<const MotionEvent*>(motion_event)->getRawXOffset();
 }
 
 float AMotionEvent_getYOffset(const AInputEvent* motion_event) {
-    return static_cast<const MotionEvent*>(motion_event)->getYOffset();
+    return static_cast<const MotionEvent*>(motion_event)->getRawYOffset();
 }
 
 float AMotionEvent_getXPrecision(const AInputEvent* motion_event) {
diff --git a/nfc/Android.bp b/nfc/Android.bp
index b6bc40d..7dd16ba 100644
--- a/nfc/Android.bp
+++ b/nfc/Android.bp
@@ -38,6 +38,7 @@
     name: "framework-nfc",
     libs: [
         "unsupportedappusage", // for android.compat.annotation.UnsupportedAppUsage
+        "framework-permission-s",
     ],
     static_libs: [
         "android.nfc.flags-aconfig-java",
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index c0db089..54f1421 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -194,13 +194,13 @@
 package android.nfc.cardemulation {
 
   public final class CardEmulation {
-    method public boolean categoryAllowsForegroundPreference(String);
+    method @Deprecated public boolean categoryAllowsForegroundPreference(String);
     method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public java.util.List<java.lang.String> getAidsForPreferredPaymentService();
     method public java.util.List<java.lang.String> getAidsForService(android.content.ComponentName, String);
-    method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public CharSequence getDescriptionForPreferredPaymentService();
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public CharSequence getDescriptionForPreferredPaymentService();
     method public static android.nfc.cardemulation.CardEmulation getInstance(android.nfc.NfcAdapter);
-    method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public String getRouteDestinationForPreferredPaymentService();
-    method public int getSelectionModeForCategory(String);
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public String getRouteDestinationForPreferredPaymentService();
+    method @Deprecated public int getSelectionModeForCategory(String);
     method public boolean isDefaultServiceForAid(android.content.ComponentName, String);
     method public boolean isDefaultServiceForCategory(android.content.ComponentName, String);
     method public boolean registerAidsForService(android.content.ComponentName, String, java.util.List<java.lang.String>);
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index ea58504..e55f540 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -27,6 +27,7 @@
 import android.annotation.UserHandleAware;
 import android.annotation.UserIdInt;
 import android.app.Activity;
+import android.app.role.RoleManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -278,13 +279,22 @@
      * @param category The category, e.g. {@link #CATEGORY_PAYMENT}
      * @return whether AIDs in the category can be handled by a service
      *         specified by the foreground app.
+     *
+     * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the
+     * Preferred Payment service is no longer valid. All routings will be done in a AID
+     * category agnostic manner.
      */
     @SuppressWarnings("NonUserGetterCalled")
+    @Deprecated
     public boolean categoryAllowsForegroundPreference(String category) {
+        Context contextAsUser = mContext.createContextAsUser(
+                UserHandle.of(UserHandle.myUserId()), 0);
+        RoleManager roleManager = contextAsUser.getSystemService(RoleManager.class);
+        if (roleManager.isRoleAvailable(RoleManager.ROLE_WALLET)) {
+            return true;
+        }
         if (CATEGORY_PAYMENT.equals(category)) {
             boolean preferForeground = false;
-            Context contextAsUser = mContext.createContextAsUser(
-                    UserHandle.of(UserHandle.myUserId()), 0);
             try {
                 preferForeground = Settings.Secure.getInt(
                         contextAsUser.getContentResolver(),
@@ -309,7 +319,12 @@
      *    to pick a service if there is a conflict.
      * @param category The category, for example {@link #CATEGORY_PAYMENT}
      * @return the selection mode for the passed in category
+     *
+     * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the
+     * Preferred Payment service is no longer valid. All routings will be done in a AID
+     * category agnostic manner.
      */
+    @Deprecated
     public int getSelectionModeForCategory(String category) {
         if (CATEGORY_PAYMENT.equals(category)) {
             boolean paymentRegistered = false;
@@ -792,8 +807,15 @@
      *                                               (e.g. eSE/eSE1, eSE2, etc.).
      *                          2. "OffHost" if the payment service does not specify secure element
      *                             name.
+     *
+     * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the
+     * Preferred Payment service is no longer valid. All routings will go to the Wallet Holder app.
+     * A payment service will be selected automatically based on registered AIDs. In the case of
+     * multiple services that register for the same payment AID, the selection will be done on
+     * an alphabetical order based on the component names.
      */
     @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
+    @Deprecated
     @Nullable
     public String getRouteDestinationForPreferredPaymentService() {
         try {
@@ -836,8 +858,15 @@
      * Returns a user-visible description of the preferred payment service.
      *
      * @return the preferred payment service description
+     *
+     * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the
+     * Preferred Payment service is no longer valid. All routings will go to the Wallet Holder app.
+     * A payment service will be selected automatically based on registered AIDs. In the case of
+     * multiple services that register for the same payment AID, the selection will be done on
+     * an alphabetical order based on the component names.
      */
     @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
+    @Deprecated
     @Nullable
     public CharSequence getDescriptionForPreferredPaymentService() {
         try {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 4e1f4ee..3363ac0 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -29,6 +29,7 @@
 import android.credentials.selection.Entry
 import android.credentials.selection.GetCredentialProviderData
 import android.credentials.selection.ProviderData
+import android.graphics.BlendMode
 import android.graphics.drawable.Icon
 import android.os.Bundle
 import android.os.CancellationSignal
@@ -353,6 +354,7 @@
         val sliceBuilder = InlineSuggestionUi
                 .newContentBuilder(pendingIntent)
                 .setTitle(displayName)
+        icon.setTintBlendMode(BlendMode.DST)
         sliceBuilder.setStartIcon(icon)
         if (primaryEntry.credentialType ==
                 CredentialType.PASSKEY && duplicateDisplayNameForPasskeys[displayName] == true) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt
index 3ebdd20..ff421bc 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt
@@ -21,63 +21,20 @@
 import android.content.Context
 import android.util.Size
 import android.widget.inline.InlinePresentationSpec
-import androidx.autofill.inline.common.TextViewStyle
-import androidx.autofill.inline.common.ViewStyle
-import androidx.autofill.inline.UiVersions
-import androidx.autofill.inline.UiVersions.Style
-import androidx.autofill.inline.v1.InlineSuggestionUi
-import androidx.core.content.ContextCompat
-import android.util.TypedValue
-import android.graphics.Typeface
-
 
 class InlinePresentationsFactory {
     companion object {
-        private const val googleSansMediumFontFamily = "google-sans-medium"
-        private const val googleSansTextFontFamily = "google-sans-text"
-        // There is no min width required for now but this is needed for the spec builder
-        private const val minInlineWidth = 5000
+        // There is no max width required for now but this is needed for the spec builder
+        private const val maxInlineWidth = 5000
 
 
         fun modifyInlinePresentationSpec(context: Context,
                                          originalSpec: InlinePresentationSpec): InlinePresentationSpec {
             return InlinePresentationSpec.Builder(Size(originalSpec.minSize.width, originalSpec
                     .minSize.height),
-                    Size(minInlineWidth, originalSpec
+                    Size(maxInlineWidth, originalSpec
                             .maxSize.height))
-                    .setStyle(UiVersions.newStylesBuilder().addStyle(getStyle(context)).build())
-                    .build()
-        }
-
-
-        fun getStyle(context: Context): Style {
-            val textColorPrimary = ContextCompat.getColor(context,
-                    com.android.credentialmanager.R.color.text_primary)
-            val textColorSecondary = ContextCompat.getColor(context,
-                    com.android.credentialmanager.R.color.text_secondary)
-            val textColorBackground = ContextCompat.getColor(context,
-                    com.android.credentialmanager.R.color.inline_background)
-            val chipHorizontalPadding = context.resources.getDimensionPixelSize(com.android
-                    .credentialmanager.R.dimen.horizontal_chip_padding)
-            val chipVerticalPadding = context.resources.getDimensionPixelSize(com.android
-                    .credentialmanager.R.dimen.vertical_chip_padding)
-            return InlineSuggestionUi.newStyleBuilder()
-                    .setChipStyle(
-                            ViewStyle.Builder().setPadding(chipHorizontalPadding,
-                                    chipVerticalPadding,
-                                    chipHorizontalPadding, chipVerticalPadding).build()
-                    )
-                    .setTitleStyle(
-                            TextViewStyle.Builder().setTextColor(textColorPrimary).setTextSize
-                            (TypedValue.COMPLEX_UNIT_DIP, 14F)
-                                    .setTypeface(googleSansMediumFontFamily,
-                                            Typeface.NORMAL).setBackgroundColor(textColorBackground)
-                                    .build()
-                    )
-                    .setSubtitleStyle(TextViewStyle.Builder().setTextColor(textColorSecondary)
-                            .setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12F).setTypeface
-                            (googleSansTextFontFamily, Typeface.NORMAL).setBackgroundColor
-                            (textColorBackground).build())
+                    .setStyle(originalSpec.getStyle())
                     .build()
         }
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 33086f98..e489bc5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -1918,16 +1918,6 @@
         }
     };
 
-    public static final AppFilter FILTER_PERSONAL_OR_PRIVATE = new AppFilter() {
-        @Override
-        public void init() {}
-
-        @Override
-        public boolean filterApp(AppEntry entry) {
-            return entry.showInPersonalTab || entry.isPrivateProfile();
-        }
-    };
-
     /**
      * Displays a combined list with "downloaded" and "visible in launcher" apps only.
      */
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
index da1fd55..0c7d6f0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
@@ -311,7 +311,7 @@
         }
 
         builder.apply {
-            setSourceDevice(device, sourceAddrType)
+            setSourceDevice(device, addrType)
             setSourceAdvertisingSid(sourceAdvertiserSid)
             setBroadcastId(broadcastId)
             setBroadcastName(broadcastName)
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 5f026c4..e34c50e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -44,7 +44,6 @@
 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
 
 import android.annotation.TargetApi;
-import android.app.Notification;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.content.ComponentName;
@@ -67,6 +66,7 @@
 import com.android.settingslib.media.flags.Flags;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
@@ -81,11 +81,43 @@
 
 /** InfoMediaManager provide interface to get InfoMediaDevice list. */
 @RequiresApi(Build.VERSION_CODES.R)
-public abstract class InfoMediaManager extends MediaManager {
+public abstract class InfoMediaManager {
+    /** Callback for notifying device is added, removed and attributes changed. */
+    public interface MediaDeviceCallback {
 
-    private static final String TAG = "InfoMediaManager";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-    protected final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
+        /**
+         * Callback for notifying MediaDevice list is added.
+         *
+         * @param devices the MediaDevice list
+         */
+        void onDeviceListAdded(@NonNull List<MediaDevice> devices);
+
+        /**
+         * Callback for notifying MediaDevice list is removed.
+         *
+         * @param devices the MediaDevice list
+         */
+        void onDeviceListRemoved(@NonNull List<MediaDevice> devices);
+
+        /**
+         * Callback for notifying connected MediaDevice is changed.
+         *
+         * @param id the id of MediaDevice
+         */
+        void onConnectedDeviceChanged(@Nullable String id);
+
+        /**
+         * Callback for notifying that transferring is failed.
+         *
+         * @param reason the reason that the request has failed. Can be one of followings: {@link
+         *     android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, {@link
+         *     android.media.MediaRoute2ProviderService#REASON_REJECTED}, {@link
+         *     android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR}, {@link
+         *     android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, {@link
+         *     android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND},
+         */
+        void onRequestFailed(int reason);
+    }
 
     /** Checked exception that signals the specified package is not present in the system. */
     public static class PackageNotAvailableException extends Exception {
@@ -94,19 +126,22 @@
         }
     }
 
+    private static final String TAG = "InfoMediaManager";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    protected final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
+    @NonNull protected final Context mContext;
     @NonNull protected final String mPackageName;
+    private final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
     private MediaDevice mCurrentConnectedDevice;
     private final LocalBluetoothManager mBluetoothManager;
     private final Map<String, RouteListingPreference.Item> mPreferenceItemMap =
             new ConcurrentHashMap<>();
 
     /* package */ InfoMediaManager(
-            Context context,
+            @NonNull Context context,
             @NonNull String packageName,
-            Notification notification,
-            LocalBluetoothManager localBluetoothManager) {
-        super(context, notification);
-
+            @NonNull LocalBluetoothManager localBluetoothManager) {
+        mContext = context;
         mBluetoothManager = localBluetoothManager;
         mPackageName = packageName;
     }
@@ -115,7 +150,6 @@
     public static InfoMediaManager createInstance(
             Context context,
             @Nullable String packageName,
-            Notification notification,
             LocalBluetoothManager localBluetoothManager) {
 
         // The caller is only interested in system routes (headsets, built-in speakers, etc), and is
@@ -127,17 +161,14 @@
 
         if (Flags.useMediaRouter2ForInfoMediaManager()) {
             try {
-                return new RouterInfoMediaManager(
-                        context, packageName, notification, localBluetoothManager);
+                return new RouterInfoMediaManager(context, packageName, localBluetoothManager);
             } catch (PackageNotAvailableException ex) {
                 // TODO: b/293578081 - Propagate this exception to callers for proper handling.
                 Log.w(TAG, "Returning a no-op InfoMediaManager for package " + packageName);
-                return new NoOpInfoMediaManager(
-                        context, packageName, notification, localBluetoothManager);
+                return new NoOpInfoMediaManager(context, packageName, localBluetoothManager);
             }
         } else {
-            return new ManagerInfoMediaManager(
-                    context, packageName, notification, localBluetoothManager);
+            return new ManagerInfoMediaManager(context, packageName, localBluetoothManager);
         }
     }
 
@@ -239,6 +270,38 @@
         return null;
     }
 
+    protected final void registerCallback(MediaDeviceCallback callback) {
+        if (!mCallbacks.contains(callback)) {
+            mCallbacks.add(callback);
+        }
+    }
+
+    protected final void unregisterCallback(MediaDeviceCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    private void dispatchDeviceListAdded(@NonNull List<MediaDevice> devices) {
+        for (MediaDeviceCallback callback : getCallbacks()) {
+            callback.onDeviceListAdded(new ArrayList<>(devices));
+        }
+    }
+
+    private void dispatchConnectedDeviceChanged(String id) {
+        for (MediaDeviceCallback callback : getCallbacks()) {
+            callback.onConnectedDeviceChanged(id);
+        }
+    }
+
+    protected void dispatchOnRequestFailed(int reason) {
+        for (MediaDeviceCallback callback : getCallbacks()) {
+            callback.onRequestFailed(reason);
+        }
+    }
+
+    private Collection<MediaDeviceCallback> getCallbacks() {
+        return new CopyOnWriteArrayList<>(mCallbacks);
+    }
+
     /**
      * Get current device that played media.
      * @return MediaDevice
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index 5925492..63056b6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -138,8 +138,7 @@
         }
 
         mInfoMediaManager =
-                InfoMediaManager.createInstance(
-                        context, packageName, notification, mLocalBluetoothManager);
+                InfoMediaManager.createInstance(context, packageName, mLocalBluetoothManager);
     }
 
     /**
@@ -505,9 +504,9 @@
         return new CopyOnWriteArrayList<>(mCallbacks);
     }
 
-    class MediaDeviceCallback implements MediaManager.MediaDeviceCallback {
+    class MediaDeviceCallback implements InfoMediaManager.MediaDeviceCallback {
         @Override
-        public void onDeviceListAdded(List<MediaDevice> devices) {
+        public void onDeviceListAdded(@NonNull List<MediaDevice> devices) {
             synchronized (mMediaDevicesLock) {
                 mMediaDevices.clear();
                 mMediaDevices.addAll(devices);
@@ -637,7 +636,7 @@
         }
 
         @Override
-        public void onDeviceListRemoved(List<MediaDevice> devices) {
+        public void onDeviceListRemoved(@NonNull List<MediaDevice> devices) {
             synchronized (mMediaDevicesLock) {
                 mMediaDevices.removeAll(devices);
             }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
index 453e807..c4fac35 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.media;
 
-import android.app.Notification;
 import android.content.Context;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
@@ -54,9 +53,8 @@
     /* package */ ManagerInfoMediaManager(
             Context context,
             @NonNull String packageName,
-            Notification notification,
             LocalBluetoothManager localBluetoothManager) {
-        super(context, packageName, notification, localBluetoothManager);
+        super(context, packageName, localBluetoothManager);
 
         mRouterManager = MediaRouter2Manager.getInstance(context);
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java
deleted file mode 100644
index d562c8a..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.media;
-
-import android.annotation.NonNull;
-import android.app.Notification;
-import android.content.Context;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * MediaManager provide interface to get MediaDevice list.
- */
-public abstract class MediaManager {
-
-    protected final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
-
-    protected Context mContext;
-    protected Notification mNotification;
-
-    MediaManager(Context context, Notification notification) {
-        mContext = context;
-        mNotification = notification;
-    }
-
-    protected void registerCallback(MediaDeviceCallback callback) {
-        if (!mCallbacks.contains(callback)) {
-            mCallbacks.add(callback);
-        }
-    }
-
-    protected void unregisterCallback(MediaDeviceCallback callback) {
-        if (mCallbacks.contains(callback)) {
-            mCallbacks.remove(callback);
-        }
-    }
-
-    protected void dispatchDeviceListAdded(@NonNull List<MediaDevice> devices) {
-        for (MediaDeviceCallback callback : getCallbacks()) {
-            callback.onDeviceListAdded(new ArrayList<>(devices));
-        }
-    }
-
-    protected void dispatchDeviceListRemoved(List<MediaDevice> devices) {
-        for (MediaDeviceCallback callback : getCallbacks()) {
-            callback.onDeviceListRemoved(devices);
-        }
-    }
-
-    protected void dispatchConnectedDeviceChanged(String id) {
-        for (MediaDeviceCallback callback : getCallbacks()) {
-            callback.onConnectedDeviceChanged(id);
-        }
-    }
-
-    protected void dispatchOnRequestFailed(int reason) {
-        for (MediaDeviceCallback callback : getCallbacks()) {
-            callback.onRequestFailed(reason);
-        }
-    }
-
-    private Collection<MediaDeviceCallback> getCallbacks() {
-        return new CopyOnWriteArrayList<>(mCallbacks);
-    }
-
-    /**
-     * Callback for notifying device is added, removed and attributes changed.
-     */
-    public interface MediaDeviceCallback {
-
-        /**
-         * Callback for notifying MediaDevice list is added.
-         *
-         * @param devices the MediaDevice list
-         */
-        void onDeviceListAdded(List<MediaDevice> devices);
-
-        /**
-         * Callback for notifying MediaDevice list is removed.
-         *
-         * @param devices the MediaDevice list
-         */
-        void onDeviceListRemoved(List<MediaDevice> devices);
-
-        /**
-         * Callback for notifying connected MediaDevice is changed.
-         *
-         * @param id the id of MediaDevice
-         */
-        void onConnectedDeviceChanged(String id);
-
-        /**
-         * Callback for notifying that transferring is failed.
-         *
-         * @param reason the reason that the request has failed. Can be one of followings:
-         * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR},
-         * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED},
-         * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR},
-         * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE},
-         * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND},
-         */
-        void onRequestFailed(int reason);
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
index ea4de39..ff4d4dd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.media;
 
-import android.app.Notification;
 import android.content.Context;
 import android.media.MediaRoute2Info;
 import android.media.RouteListingPreference;
@@ -42,9 +41,8 @@
     NoOpInfoMediaManager(
             Context context,
             @NonNull String packageName,
-            Notification notification,
             LocalBluetoothManager localBluetoothManager) {
-        super(context, packageName, notification, localBluetoothManager);
+        super(context, packageName, localBluetoothManager);
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
index df03167..9c82cb1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
@@ -17,7 +17,6 @@
 package com.android.settingslib.media;
 
 import android.annotation.SuppressLint;
-import android.app.Notification;
 import android.content.Context;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2;
@@ -71,10 +70,9 @@
     /* package */ RouterInfoMediaManager(
             Context context,
             @NonNull String packageName,
-            Notification notification,
             LocalBluetoothManager localBluetoothManager)
             throws PackageNotAvailableException {
-        super(context, packageName, notification, localBluetoothManager);
+        super(context, packageName, localBluetoothManager);
 
         MediaRouter2 router = null;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
index 0117ece..d5444cf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
@@ -53,7 +53,7 @@
          * @param noInternet True if a connected Wi-Fi network cannot access the Internet
          * @param level The number of bars to show (0-4)
          */
-        fun getIcon(noInternet: Boolean, level: Int): Drawable? {
+        open fun getIcon(noInternet: Boolean, level: Int): Drawable? {
             return context.getDrawable(getInternetIconResource(level, noInternet))
         }
     }
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java
index c647cbb..f0185b9 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/InfoMediaManagerIntegTest.java
@@ -64,22 +64,21 @@
     @RequiresFlagsEnabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER)
     public void createInstance_withMR2FlagOn_returnsRouterInfoMediaManager() {
         InfoMediaManager manager =
-                InfoMediaManager.createInstance(mContext, mContext.getPackageName(), null, null);
+                InfoMediaManager.createInstance(mContext, mContext.getPackageName(), null);
         assertThat(manager).isInstanceOf(RouterInfoMediaManager.class);
     }
 
     @Test
     @RequiresFlagsEnabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER)
     public void createInstance_withMR2FlagOn_withFakePackage_returnsNoOpInfoMediaManager() {
-        InfoMediaManager manager =
-                InfoMediaManager.createInstance(mContext, FAKE_PACKAGE, null, null);
+        InfoMediaManager manager = InfoMediaManager.createInstance(mContext, FAKE_PACKAGE, null);
         assertThat(manager).isInstanceOf(NoOpInfoMediaManager.class);
     }
 
     @Test
     @RequiresFlagsEnabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER)
     public void createInstance_withMR2FlagOn_withNullPackage_returnsRouterInfoMediaManager() {
-        InfoMediaManager manager = InfoMediaManager.createInstance(mContext, null, null, null);
+        InfoMediaManager manager = InfoMediaManager.createInstance(mContext, null, null);
         assertThat(manager).isInstanceOf(RouterInfoMediaManager.class);
     }
 
@@ -87,7 +86,7 @@
     @RequiresFlagsDisabled(FLAG_USE_MEDIA_ROUTER2_FOR_INFO_MEDIA_MANAGER)
     public void createInstance_withMR2FlagOff_returnsManagerInfoMediaManager() {
         InfoMediaManager manager =
-                InfoMediaManager.createInstance(mContext, mContext.getPackageName(), null, null);
+                InfoMediaManager.createInstance(mContext, mContext.getPackageName(), null);
         assertThat(manager).isInstanceOf(ManagerInfoMediaManager.class);
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
index 827d8fa..3b18aa3 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
@@ -72,6 +72,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -499,6 +500,7 @@
         verify(mApplicationsState, never()).clearEntries();
     }
 
+    @Ignore("b/328332487")
     @Test
     public void removeProfileApp_workprofileExists_doResumeIfNeededLocked_shouldClearEntries()
             throws RemoteException {
@@ -573,6 +575,7 @@
         verify(mApplicationsState).clearEntries();
     }
 
+    @Ignore("b/328332487")
     @Test
     public void removeOwnerApp_workprofileExists_doResumeIfNeededLocked_shouldClearEntries()
             throws RemoteException {
@@ -654,6 +657,7 @@
         verify(mApplicationsState).clearEntries();
     }
 
+    @Ignore("b/328332487")
     @Test
     public void noAppRemoved_workprofileExists_doResumeIfNeededLocked_shouldNotClearEntries()
             throws RemoteException {
@@ -773,6 +777,7 @@
         assertThat(primaryUserApp.shouldShowInPersonalTab(um, appInfo.uid)).isTrue();
     }
 
+    @Ignore("b/328332487")
     @Test
     public void shouldShowInPersonalTab_userProfilePreU_returnsFalse() {
         UserManager um = RuntimeEnvironment.application.getSystemService(UserManager.class);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index c159d5e..d85d253 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -91,7 +91,7 @@
     @Mock
     private LocalBluetoothManager mLocalBluetoothManager;
     @Mock
-    private MediaManager.MediaDeviceCallback mCallback;
+    private InfoMediaManager.MediaDeviceCallback mCallback;
     @Mock
     private MediaSessionManager mMediaSessionManager;
     @Mock
@@ -109,8 +109,7 @@
         doReturn(mMediaSessionManager).when(mContext).getSystemService(
                 Context.MEDIA_SESSION_SERVICE);
         mInfoMediaManager =
-                new ManagerInfoMediaManager(
-                        mContext, TEST_PACKAGE_NAME, null, mLocalBluetoothManager);
+                new ManagerInfoMediaManager(mContext, TEST_PACKAGE_NAME, mLocalBluetoothManager);
         mShadowRouter2Manager = ShadowRouter2Manager.getShadow();
         mInfoMediaManager.mRouterManager = MediaRouter2Manager.getInstance(mContext);
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index 9a7d4f1..693b7d0 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -35,7 +36,6 @@
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.MediaRoute2Info;
-import android.media.MediaRouter2Manager;
 import android.media.RoutingSessionInfo;
 
 import com.android.settingslib.bluetooth.A2dpProfile;
@@ -75,8 +75,6 @@
     private static final String TEST_ADDRESS = "00:01:02:03:04:05";
 
     @Mock
-    private InfoMediaManager mInfoMediaManager;
-    @Mock
     private LocalBluetoothManager mLocalBluetoothManager;
     @Mock
     private LocalMediaManager.DeviceCallback mCallback;
@@ -87,8 +85,6 @@
     @Mock
     private LocalBluetoothProfileManager mLocalProfileManager;
     @Mock
-    private MediaRouter2Manager mMediaRouter2Manager;
-    @Mock
     private MediaRoute2Info mRouteInfo1;
     @Mock
     private MediaRoute2Info mRouteInfo2;
@@ -97,6 +93,7 @@
 
     private Context mContext;
     private LocalMediaManager mLocalMediaManager;
+    private InfoMediaManager mInfoMediaManager;
     private ShadowBluetoothAdapter mShadowBluetoothAdapter;
     private InfoMediaDevice mInfoMediaDevice1;
     private InfoMediaDevice mInfoMediaDevice2;
@@ -116,10 +113,16 @@
         when(mLocalProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
         when(mLocalProfileManager.getHearingAidProfile()).thenReturn(mHapProfile);
 
+        // Need to call constructor to initialize final fields.
+        mInfoMediaManager = mock(
+                InfoMediaManager.class,
+                withSettings().useConstructor(mContext, TEST_PACKAGE_NAME, mLocalBluetoothManager));
+
         mInfoMediaDevice1 = spy(new InfoMediaDevice(mContext, mRouteInfo1));
         mInfoMediaDevice2 = new InfoMediaDevice(mContext, mRouteInfo2);
-        mLocalMediaManager = new LocalMediaManager(mContext, mLocalBluetoothManager,
-                mInfoMediaManager, "com.test.packagename");
+        mLocalMediaManager =
+                new LocalMediaManager(
+                        mContext, mLocalBluetoothManager, mInfoMediaManager, TEST_PACKAGE_NAME);
         mLocalMediaManager.mAudioManager = mAudioManager;
     }
 
@@ -146,7 +149,6 @@
 
         mLocalMediaManager.registerCallback(mCallback);
         assertThat(mLocalMediaManager.connectDevice(device)).isTrue();
-
         verify(mInfoMediaManager).connectToDevice(device);
     }
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaManagerTest.java
deleted file mode 100644
index c3237f0..0000000
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaManagerTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 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.
- */
-
-package com.android.settingslib.media;
-
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-import java.util.Collections;
-
-@RunWith(RobolectricTestRunner.class)
-public class MediaManagerTest {
-
-    private static final String TEST_ID = "test_id";
-
-    @Mock
-    private MediaManager.MediaDeviceCallback mCallback;
-    @Mock
-    private MediaDevice mDevice;
-
-    private MediaManager mMediaManager;
-    private Context mContext;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mContext = RuntimeEnvironment.application;
-
-        when(mDevice.getId()).thenReturn(TEST_ID);
-
-        mMediaManager = new MediaManager(mContext, null) {};
-    }
-
-    @Test
-    public void dispatchDeviceListAdded_registerCallback_shouldDispatchCallback() {
-        mMediaManager.registerCallback(mCallback);
-
-        mMediaManager.dispatchDeviceListAdded(Collections.emptyList());
-
-        verify(mCallback).onDeviceListAdded(any());
-    }
-
-    @Test
-    public void dispatchDeviceListRemoved_registerCallback_shouldDispatchCallback() {
-        mMediaManager.registerCallback(mCallback);
-
-        mMediaManager.dispatchDeviceListRemoved(Collections.emptyList());
-
-        verify(mCallback).onDeviceListRemoved(Collections.emptyList());
-    }
-
-    @Test
-    public void dispatchActiveDeviceChanged_registerCallback_shouldDispatchCallback() {
-        mMediaManager.registerCallback(mCallback);
-
-        mMediaManager.dispatchConnectedDeviceChanged(TEST_ID);
-
-        verify(mCallback).onConnectedDeviceChanged(TEST_ID);
-    }
-
-    @Test
-    public void dispatchOnRequestFailed_registerCallback_shouldDispatchCallback() {
-        mMediaManager.registerCallback(mCallback);
-
-        mMediaManager.dispatchOnRequestFailed(1);
-
-        verify(mCallback).onRequestFailed(1);
-    }
-
-}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index add3134..8f8445d 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -115,6 +115,10 @@
         Settings.Global.Wearable.SCREEN_UNLOCK_SOUND_ENABLED,
         Settings.Global.Wearable.CHARGING_SOUNDS_ENABLED,
         Settings.Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED,
+        Settings.Global.Wearable.AUTO_BEDTIME_MODE,
         Settings.Global.FORCE_ENABLE_PSS_PROFILING,
+        Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_ENABLED,
+        Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_TYPE,
+        Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_SPEED,
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index c0a0760..6def40b 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -452,6 +452,7 @@
         VALIDATORS.put(Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(
                 Global.Wearable.CONSISTENT_NOTIFICATION_BLOCKING_ENABLED, ANY_INTEGER_VALIDATOR);
+        VALIDATORS.put(Global.Wearable.AUTO_BEDTIME_MODE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.FORCE_ENABLE_PSS_PROFILING, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 28cdc6d..09d076e 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -626,9 +626,6 @@
                     Settings.Global.Wearable.BEDTIME_MODE,
                     Settings.Global.Wearable.BEDTIME_HARD_MODE,
                     Settings.Global.Wearable.LOCK_SCREEN_STATE,
-                    Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_ENABLED,
-                    Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_TYPE,
-                    Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_SPEED,
                     Settings.Global.Wearable.DISABLE_AOD_WHILE_PLUGGED,
                     Settings.Global.Wearable.NETWORK_LOCATION_OPT_IN,
                     Settings.Global.Wearable.CUSTOM_COLOR_FOREGROUND,
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index baa1397..d05d40d 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -25,6 +25,16 @@
 }
 
 flag {
+   name: "notification_view_flipper_pausing"
+   namespace: "systemui"
+   description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed."
+   bug: "309146176"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
     name: "notification_async_group_header_inflation"
     namespace: "systemui"
     description: "Inflates the notification group summary header views from the background thread."
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 8dc7495..b124025 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -27,6 +27,7 @@
 import android.text.Layout
 import android.util.LruCache
 import kotlin.math.roundToInt
+import android.util.Log
 
 private const val DEFAULT_ANIMATION_DURATION: Long = 300
 private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
@@ -140,7 +141,6 @@
     }
 
     sealed class PositionedGlyph {
-
         /** Mutable X coordinate of the glyph position relative from drawing offset. */
         var x: Float = 0f
 
@@ -269,41 +269,53 @@
         duration: Long = -1L,
         interpolator: TimeInterpolator? = null,
         delay: Long = 0,
-        onAnimationEnd: Runnable? = null
+        onAnimationEnd: Runnable? = null,
+    ) = setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration,
+        interpolator, delay, onAnimationEnd, updateLayoutOnFailure = true)
+
+    private fun setTextStyleInternal(
+        fvar: String?,
+        textSize: Float,
+        color: Int?,
+        strokeWidth: Float,
+        animate: Boolean,
+        duration: Long,
+        interpolator: TimeInterpolator?,
+        delay: Long,
+        onAnimationEnd: Runnable?,
+        updateLayoutOnFailure: Boolean,
     ) {
-        if (animate) {
-            animator.cancel()
-            textInterpolator.rebase()
-        }
+        try {
+            if (animate) {
+                animator.cancel()
+                textInterpolator.rebase()
+            }
 
-        if (textSize >= 0) {
-            textInterpolator.targetPaint.textSize = textSize
-        }
+            if (textSize >= 0) {
+                textInterpolator.targetPaint.textSize = textSize
+            }
+            if (!fvar.isNullOrBlank()) {
+                textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
+            }
+            if (color != null) {
+                textInterpolator.targetPaint.color = color
+            }
+            if (strokeWidth >= 0F) {
+                textInterpolator.targetPaint.strokeWidth = strokeWidth
+            }
+            textInterpolator.onTargetPaintModified()
 
-        if (!fvar.isNullOrBlank()) {
-            textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
-        }
-
-        if (color != null) {
-            textInterpolator.targetPaint.color = color
-        }
-        if (strokeWidth >= 0F) {
-            textInterpolator.targetPaint.strokeWidth = strokeWidth
-        }
-        textInterpolator.onTargetPaintModified()
-
-        if (animate) {
-            animator.startDelay = delay
-            animator.duration =
-                if (duration == -1L) {
-                    DEFAULT_ANIMATION_DURATION
-                } else {
-                    duration
-                }
-            interpolator?.let { animator.interpolator = it }
-            if (onAnimationEnd != null) {
-                val listener =
-                    object : AnimatorListenerAdapter() {
+            if (animate) {
+                animator.startDelay = delay
+                animator.duration =
+                    if (duration == -1L) {
+                        DEFAULT_ANIMATION_DURATION
+                    } else {
+                        duration
+                    }
+                interpolator?.let { animator.interpolator = it }
+                if (onAnimationEnd != null) {
+                    val listener = object : AnimatorListenerAdapter() {
                         override fun onAnimationEnd(animation: Animator) {
                             onAnimationEnd.run()
                             animator.removeListener(this)
@@ -312,14 +324,25 @@
                             animator.removeListener(this)
                         }
                     }
-                animator.addListener(listener)
+                    animator.addListener(listener)
+                }
+                animator.start()
+            } else {
+                // No animation is requested, thus set base and target state to the same state.
+                textInterpolator.progress = 1f
+                textInterpolator.rebase()
+                invalidateCallback()
             }
-            animator.start()
-        } else {
-            // No animation is requested, thus set base and target state to the same state.
-            textInterpolator.progress = 1f
-            textInterpolator.rebase()
-            invalidateCallback()
+        } catch (ex: IllegalArgumentException) {
+            if (updateLayoutOnFailure) {
+                Log.e(TAG, "setTextStyleInternal: Exception caught but retrying. This is usually" +
+                    " due to the layout having changed unexpectedly without being notified.", ex)
+                updateLayout(textInterpolator.layout)
+                setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration,
+                    interpolator, delay, onAnimationEnd, updateLayoutOnFailure = false)
+            } else {
+                throw ex
+            }
         }
     }
 
@@ -355,15 +378,13 @@
         interpolator: TimeInterpolator? = null,
         delay: Long = 0,
         onAnimationEnd: Runnable? = null
-    ) {
-        val fvar = fontVariationUtils.updateFontVariation(
-            weight = weight,
-            width = width,
-            opticalSize = opticalSize,
-            roundness = roundness,
-        )
-        setTextStyle(
-            fvar = fvar,
+    ) = setTextStyleInternal(
+            fvar = fontVariationUtils.updateFontVariation(
+                weight = weight,
+                width = width,
+                opticalSize = opticalSize,
+                roundness = roundness,
+            ),
             textSize = textSize,
             color = color,
             strokeWidth = strokeWidth,
@@ -372,6 +393,10 @@
             interpolator = interpolator,
             delay = delay,
             onAnimationEnd = onAnimationEnd,
+            updateLayoutOnFailure = true,
         )
+
+    companion object {
+        private val TAG = TextAnimator::class.simpleName!!
     }
 }
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 515c816..078da1c86 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
@@ -31,8 +31,8 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -93,7 +93,6 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
@@ -119,7 +118,6 @@
 import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.res.R
 import kotlinx.coroutines.launch
@@ -199,37 +197,26 @@
                     }
                 },
     ) {
-        Column(Modifier.align(Alignment.TopStart)) {
-            CommunalHubLazyGrid(
-                communalContent = communalContent,
-                viewModel = viewModel,
-                contentPadding = contentPadding,
-                contentOffset = contentOffset,
-                setGridCoordinates = { gridCoordinates = it },
-                updateDragPositionForRemove = { offset ->
-                    isDraggingToRemove =
-                        isPointerWithinCoordinates(
-                            offset = gridCoordinates?.let { it.positionInWindow() + offset },
-                            containerToCheck = removeButtonCoordinates
-                        )
-                    isDraggingToRemove
-                },
-                onOpenWidgetPicker = onOpenWidgetPicker,
-                gridState = gridState,
-                contentListState = contentListState,
-                selectedKey = selectedKey,
-                widgetConfigurator = widgetConfigurator,
-            )
-            // TODO(b/326060686): Remove this once keyguard indication area can persist over hub
-            if (viewModel is CommunalViewModel) {
-                val isUnlocked by viewModel.deviceUnlocked.collectAsState(initial = false)
-                Spacer(Modifier.height(24.dp))
-                LockStateIcon(
-                    isUnlocked = isUnlocked,
-                    modifier = Modifier.align(Alignment.CenterHorizontally),
-                )
-            }
-        }
+        CommunalHubLazyGrid(
+            communalContent = communalContent,
+            viewModel = viewModel,
+            contentPadding = contentPadding,
+            contentOffset = contentOffset,
+            setGridCoordinates = { gridCoordinates = it },
+            updateDragPositionForRemove = { offset ->
+                isDraggingToRemove =
+                    isPointerWithinCoordinates(
+                        offset = gridCoordinates?.let { it.positionInWindow() + offset },
+                        containerToCheck = removeButtonCoordinates
+                    )
+                isDraggingToRemove
+            },
+            onOpenWidgetPicker = onOpenWidgetPicker,
+            gridState = gridState,
+            contentListState = contentListState,
+            selectedKey = selectedKey,
+            widgetConfigurator = widgetConfigurator,
+        )
 
         if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
             Toolbar(
@@ -281,7 +268,7 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun ColumnScope.CommunalHubLazyGrid(
+private fun BoxScope.CommunalHubLazyGrid(
     communalContent: List<CommunalContentModel>,
     viewModel: BaseCommunalViewModel,
     contentPadding: PaddingValues,
@@ -295,7 +282,7 @@
     widgetConfigurator: WidgetConfigurator?,
 ) {
     var gridModifier =
-        Modifier.align(Alignment.Start).onGloballyPositioned { setGridCoordinates(it) }
+        Modifier.align(Alignment.TopStart).onGloballyPositioned { setGridCoordinates(it) }
     var list = communalContent
     var dragDropState: GridDragDropState? = null
     if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) {
@@ -377,26 +364,6 @@
     }
 }
 
-@Composable
-private fun LockStateIcon(
-    isUnlocked: Boolean,
-    modifier: Modifier = Modifier,
-) {
-    val colors = LocalAndroidColorScheme.current
-    val resource =
-        if (isUnlocked) {
-            R.drawable.ic_unlocked
-        } else {
-            R.drawable.ic_lock
-        }
-    Icon(
-        painter = painterResource(id = resource),
-        contentDescription = null,
-        tint = colors.onPrimaryContainer,
-        modifier = modifier.size(52.dp)
-    )
-}
-
 /**
  * Toolbar that contains action buttons to
  * 1) open the widget picker
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 92eb8f8..4950b96 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -45,32 +45,32 @@
 import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
-import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
+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.fakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -102,6 +102,7 @@
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper(setAsMainLooper = true)
 class UdfpsControllerOverlayTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
 
     @JvmField @Rule var rule = MockitoJUnit.rule()
 
@@ -148,28 +149,11 @@
 
     @Before
     fun setup() {
-        testScope = TestScope(StandardTestDispatcher())
-        powerRepository = FakePowerRepository()
-        powerInteractor =
-            PowerInteractor(
-                powerRepository,
-                mock(FalsingCollector::class.java),
-                mock(ScreenOffAnimationController::class.java),
-                statusBarStateController,
-            )
-        keyguardTransitionRepository = FakeKeyguardTransitionRepository()
-        keyguardTransitionInteractor =
-            KeyguardTransitionInteractor(
-                scope = testScope.backgroundScope,
-                repository = keyguardTransitionRepository,
-                fromLockscreenTransitionInteractor = {
-                    mock(FromLockscreenTransitionInteractor::class.java)
-                },
-                fromPrimaryBouncerTransitionInteractor = {
-                    mock(FromPrimaryBouncerTransitionInteractor::class.java)
-                },
-                fromAodTransitionInteractor = { mock(FromAodTransitionInteractor::class.java) },
-            )
+        testScope = kosmos.testScope
+        powerRepository = kosmos.fakePowerRepository
+        powerInteractor = kosmos.powerInteractor
+        keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+        keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
         whenever(inflater.inflate(R.layout.udfps_view, null, false)).thenReturn(udfpsView)
         whenever(inflater.inflate(R.layout.udfps_bp_view, null))
             .thenReturn(mock(UdfpsBpView::class.java))
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 8f802b8..563aad1 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
@@ -39,7 +39,6 @@
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -114,7 +113,6 @@
                 kosmos.communalInteractor,
                 kosmos.communalTutorialInteractor,
                 kosmos.shadeInteractor,
-                kosmos.deviceEntryInteractor,
                 mediaHost,
                 logcatLogBuffer("CommunalViewModelTest"),
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index fb46ed9d..b3380ff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -25,18 +25,17 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LightRevealEffect
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -49,9 +48,10 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 class LightRevealScrimRepositoryTest : SysuiTestCase() {
-    private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
-    private lateinit var powerRepository: FakePowerRepository
-    private lateinit var powerInteractor: PowerInteractor
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
+    private val powerInteractor = kosmos.powerInteractor
     private lateinit var underTest: LightRevealScrimRepositoryImpl
 
     @get:Rule val animatorTestRule = AnimatorTestRule(this)
@@ -59,13 +59,13 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        fakeKeyguardRepository = FakeKeyguardRepository()
-        powerRepository = FakePowerRepository()
-        powerInteractor =
-            PowerInteractorFactory.create(repository = powerRepository).powerInteractor
-
         underTest =
-            LightRevealScrimRepositoryImpl(fakeKeyguardRepository, context, powerInteractor, mock())
+            LightRevealScrimRepositoryImpl(
+                kosmos.fakeKeyguardRepository,
+                context,
+                kosmos.powerInteractor,
+                mock()
+            )
     }
 
     @Test
@@ -168,8 +168,9 @@
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     fun revealAmount_emitsTo1AfterAnimationStarted() =
-        runTest(UnconfinedTestDispatcher()) {
+        testScope.runTest {
             val value by collectLastValue(underTest.revealAmount)
+            runCurrent()
             underTest.startRevealAmountAnimator(true)
             assertEquals(0.0f, value)
             animatorTestRule.advanceTimeBy(500L)
@@ -179,8 +180,9 @@
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     fun revealAmount_startingRevealTwiceWontRerunAnimator() =
-        runTest(UnconfinedTestDispatcher()) {
+        testScope.runTest {
             val value by collectLastValue(underTest.revealAmount)
+            runCurrent()
             underTest.startRevealAmountAnimator(true)
             assertEquals(0.0f, value)
             animatorTestRule.advanceTimeBy(250L)
@@ -193,12 +195,14 @@
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     fun revealAmount_emitsTo0AfterAnimationStartedReversed() =
-        runTest(UnconfinedTestDispatcher()) {
-            val value by collectLastValue(underTest.revealAmount)
-            underTest.startRevealAmountAnimator(false)
-            assertEquals(1.0f, value)
+        testScope.runTest {
+            val lastValue by collectLastValue(underTest.revealAmount)
+            runCurrent()
+            underTest.startRevealAmountAnimator(true)
             animatorTestRule.advanceTimeBy(500L)
-            assertEquals(0.0f, value)
+            underTest.startRevealAmountAnimator(false)
+            animatorTestRule.advanceTimeBy(500L)
+            assertEquals(0.0f, lastValue)
         }
 
     /**
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 f9ec3d1..24c651f3 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
@@ -18,9 +18,11 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.app.StatusBarManager
+import android.platform.test.annotations.DisableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
@@ -120,6 +122,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun testKeyguardGuardVisibilityStopsSecureCamera() =
         testScope.runTest {
             val secureCameraActive = collectLastValue(underTest.isSecureCameraActive)
@@ -144,6 +147,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun testBouncerShowingResetsSecureCameraState() =
         testScope.runTest {
             val secureCameraActive = collectLastValue(underTest.isSecureCameraActive)
@@ -166,6 +170,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun keyguardVisibilityIsDefinedAsKeyguardShowingButNotOccluded() = runTest {
         val isVisible = collectLastValue(underTest.isKeyguardVisible)
         repository.setKeyguardShowing(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileDataInteractorTest.kt
new file mode 100644
index 0000000..c80eb13
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileDataInteractorTest.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.qs.tiles.impl.battery.doman.interactor
+
+import android.os.UserHandle
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.testing.LeakCheck
+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.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.battery.domain.interactor.BatterySaverTileDataInteractor
+import com.android.systemui.utils.leaks.FakeBatteryController
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toCollection
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class BatterySaverTileDataInteractorTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+    private val batteryController = FakeBatteryController(LeakCheck())
+    private val testUser = UserHandle.of(1)
+    private val underTest =
+        BatterySaverTileDataInteractor(testScope.testScheduler, batteryController)
+
+    @Test
+    fun availability_isTrue() =
+        testScope.runTest {
+            val availability = underTest.availability(testUser).toCollection(mutableListOf())
+
+            Truth.assertThat(availability).hasSize(1)
+            Truth.assertThat(availability.last()).isTrue()
+        }
+
+    @Test
+    fun tileData_matchesBatteryControllerPowerSaving() =
+        testScope.runTest {
+            val data by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            runCurrent()
+            Truth.assertThat(data!!.isPowerSaving).isFalse()
+
+            batteryController.setPowerSaveMode(true)
+            runCurrent()
+            Truth.assertThat(data!!.isPowerSaving).isTrue()
+
+            batteryController.setPowerSaveMode(false)
+            runCurrent()
+            Truth.assertThat(data!!.isPowerSaving).isFalse()
+        }
+
+    @Test
+    fun tileData_matchesBatteryControllerIsPluggedIn() =
+        testScope.runTest {
+            val data by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            runCurrent()
+            Truth.assertThat(data!!.isPluggedIn).isFalse()
+
+            batteryController.isPluggedIn = true
+            runCurrent()
+            Truth.assertThat(data!!.isPluggedIn).isTrue()
+
+            batteryController.isPluggedIn = false
+            runCurrent()
+            Truth.assertThat(data!!.isPluggedIn).isFalse()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..62c51e6
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/doman/interactor/BatterySaverTileUserActionInteractorTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.doman.interactor
+
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.battery.domain.interactor.BatterySaverTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.utils.leaks.FakeBatteryController
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class BatterySaverTileUserActionInteractorTest : SysuiTestCase() {
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+    private val controller = FakeBatteryController(LeakCheck())
+    private val underTest = BatterySaverTileUserActionInteractor(inputHandler, controller)
+
+    @Test
+    fun handleClickWhenNotPluggedIn_flipsPowerSaverMode() = runTest {
+        val originalPowerSaveMode = controller.isPowerSave
+        controller.isPluggedIn = false
+
+        underTest.handleInput(
+            QSTileInputTestKtx.click(BatterySaverTileModel.Standard(false, originalPowerSaveMode))
+        )
+
+        Truth.assertThat(controller.isPowerSave).isNotEqualTo(originalPowerSaveMode)
+    }
+
+    @Test
+    fun handleClickWhenPluggedIn_doesNotTurnOnPowerSaverMode() = runTest {
+        controller.setPowerSaveMode(false)
+        val originalPowerSaveMode = controller.isPowerSave
+        controller.isPluggedIn = true
+
+        underTest.handleInput(
+            QSTileInputTestKtx.click(
+                BatterySaverTileModel.Standard(controller.isPluggedIn, originalPowerSaveMode)
+            )
+        )
+
+        Truth.assertThat(controller.isPowerSave).isEqualTo(originalPowerSaveMode)
+    }
+
+    @Test
+    fun handleLongClick() = runTest {
+        underTest.handleInput(
+            QSTileInputTestKtx.longClick(BatterySaverTileModel.Standard(false, false))
+        )
+
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+            Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_BATTERY_SAVER_SETTINGS)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt
new file mode 100644
index 0000000..6e9db2c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.ui
+
+import android.graphics.drawable.TestStubDrawable
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BatterySaverTileMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val batterySaverTileConfig = kosmos.qsBatterySaverTileConfig
+    private lateinit var mapper: BatterySaverTileMapper
+
+    @Before
+    fun setup() {
+        mapper =
+            BatterySaverTileMapper(
+                context.orCreateTestableResources
+                    .apply {
+                        addOverride(R.drawable.qs_battery_saver_icon_off, TestStubDrawable())
+                        addOverride(R.drawable.qs_battery_saver_icon_on, TestStubDrawable())
+                    }
+                    .resources,
+                context.theme,
+            )
+    }
+
+    @Test
+    fun map_standard_notPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Standard(false, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.INACTIVE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_standard_notPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Standard(false, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.ACTIVE,
+                "",
+                R.drawable.qs_battery_saver_icon_on,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_standard_pluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Standard(true, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_on,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_standard_pluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Standard(true, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverDisabledNotPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(false, false, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.INACTIVE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverDisabledNotPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(false, true, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.standard_battery_saver_text),
+                R.drawable.qs_battery_saver_icon_on,
+                context.getString(R.string.standard_battery_saver_text),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverDisabledPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(true, true, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_on,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverDisabledPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(true, false, false)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverEnabledNotPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(false, false, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.INACTIVE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverEnabledNotPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(false, true, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.extreme_battery_saver_text),
+                R.drawable.qs_battery_saver_icon_on,
+                context.getString(R.string.extreme_battery_saver_text),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverEnabledPluggedInPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(true, true, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_on,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun map_extremeSaverEnabledPluggedInNotPowerSaving() {
+        val inputModel = BatterySaverTileModel.Extreme(true, false, true)
+
+        val outputState = mapper.map(batterySaverTileConfig, inputModel)
+
+        val expectedState =
+            createBatterySaverTileState(
+                QSTileState.ActivationState.UNAVAILABLE,
+                "",
+                R.drawable.qs_battery_saver_icon_off,
+                null,
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    private fun createBatterySaverTileState(
+        activationState: QSTileState.ActivationState,
+        secondaryLabel: String,
+        iconRes: Int,
+        stateDescription: CharSequence?,
+    ): QSTileState {
+        val label = context.getString(R.string.battery_detail_switch_title)
+        return QSTileState(
+            { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            label,
+            activationState,
+            secondaryLabel,
+            if (activationState == QSTileState.ActivationState.UNAVAILABLE)
+                setOf(QSTileState.UserAction.LONG_CLICK)
+            else setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+            label,
+            stateDescription,
+            QSTileState.SideViewIcon.None,
+            QSTileState.EnabledState.ENABLED,
+            Switch::class.qualifiedName
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
new file mode 100644
index 0000000..39755bf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain
+
+import android.graphics.drawable.TestStubDrawable
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InternetTileMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val internetTileConfig = kosmos.qsInternetTileConfig
+    private val mapper by lazy {
+        InternetTileMapper(
+            context.orCreateTestableResources
+                .apply {
+                    addOverride(R.drawable.ic_qs_no_internet_unavailable, TestStubDrawable())
+                    addOverride(wifiRes, TestStubDrawable())
+                }
+                .resources,
+            context.theme,
+            context
+        )
+    }
+
+    @Test
+    fun withActiveModel_mappedStateMatchesDataModel() {
+        val inputModel =
+            InternetTileModel.Active(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+                iconId = wifiRes,
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_internet_label),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.quick_settings_networks_available),
+                Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null),
+                context.getString(R.string.quick_settings_internet_label)
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun withInactiveModel_mappedStateMatchesDataModel() {
+        val inputModel =
+            InternetTileModel.Inactive(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.INACTIVE,
+                context.getString(R.string.quick_settings_networks_unavailable),
+                Icon.Loaded(
+                    context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!,
+                    contentDescription = null
+                ),
+                context.getString(R.string.quick_settings_networks_unavailable)
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    private fun createInternetTileState(
+        activationState: QSTileState.ActivationState,
+        secondaryLabel: String,
+        icon: Icon,
+        contentDescription: String,
+    ): QSTileState {
+        val label = context.getString(R.string.quick_settings_internet_label)
+        return QSTileState(
+            { icon },
+            label,
+            activationState,
+            secondaryLabel,
+            setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+            contentDescription,
+            null,
+            QSTileState.SideViewIcon.Chevron,
+            QSTileState.EnabledState.ENABLED,
+            Switch::class.qualifiedName
+        )
+    }
+
+    private companion object {
+        val wifiRes = WIFI_FULL_ICONS[4]
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
new file mode 100644
index 0000000..37ef6ad
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain.interactor
+
+import android.graphics.drawable.TestStubDrawable
+import android.os.UserHandle
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.AccessibilityContentDescriptions
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.WifiIcons
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
+import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@TestableLooper.RunWithLooper
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InternetTileDataInteractorTest : SysuiTestCase() {
+    private val testUser = UserHandle.of(1)
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+
+    private lateinit var underTest: InternetTileDataInteractor
+    private lateinit var mobileIconsInteractor: MobileIconsInteractor
+
+    private val airplaneModeRepository = FakeAirplaneModeRepository()
+    private val connectivityRepository = FakeConnectivityRepository()
+    private val ethernetInteractor = EthernetInteractor(connectivityRepository)
+    private val wifiRepository = FakeWifiRepository()
+    private val userSetupRepo = FakeUserSetupRepository()
+    private val wifiInteractor =
+        WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope)
+
+    private val tableLogBuffer: TableLogBuffer = mock()
+    private val carrierConfigTracker: CarrierConfigTracker = mock()
+
+    private val mobileConnectionsRepository =
+        FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogBuffer)
+    private val mobileConnectionRepository =
+        FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer)
+
+    private val flags =
+        FakeFeatureFlagsClassic().also {
+            it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
+        }
+
+    private val internet = context.getString(R.string.quick_settings_internet_label)
+
+    @Before
+    fun setUp() {
+        mobileConnectionRepository.apply {
+            setNetworkTypeKey(mobileConnectionsRepository.GSM_KEY)
+            isInService.value = true
+            dataConnectionState.value = DataConnectionState.Connected
+            dataEnabled.value = true
+        }
+
+        mobileConnectionsRepository.apply {
+            activeMobileDataRepository.value = mobileConnectionRepository
+            activeMobileDataSubscriptionId.value = SUB_1_ID
+            setMobileConnectionRepositoryMap(mapOf(SUB_1_ID to mobileConnectionRepository))
+        }
+
+        mobileIconsInteractor =
+            MobileIconsInteractorImpl(
+                mobileConnectionsRepository,
+                carrierConfigTracker,
+                tableLogBuffer,
+                connectivityRepository,
+                userSetupRepo,
+                testScope.backgroundScope,
+                context,
+                flags,
+            )
+
+        context.orCreateTestableResources.apply {
+            addOverride(com.android.internal.R.drawable.ic_signal_cellular, TestStubDrawable())
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_0,
+                TestStubDrawable()
+            )
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_1,
+                TestStubDrawable()
+            )
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_2,
+                TestStubDrawable()
+            )
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_3,
+                TestStubDrawable()
+            )
+            addOverride(
+                com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_4,
+                TestStubDrawable()
+            )
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_phone, TestStubDrawable())
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_laptop, TestStubDrawable())
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_tablet, TestStubDrawable())
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_watch, TestStubDrawable())
+            addOverride(com.android.settingslib.R.drawable.ic_hotspot_auto, TestStubDrawable())
+        }
+
+        underTest =
+            InternetTileDataInteractor(
+                context,
+                testScope.backgroundScope,
+                airplaneModeRepository,
+                connectivityRepository,
+                ethernetInteractor,
+                mobileIconsInteractor,
+                wifiInteractor,
+            )
+    }
+
+    @Test
+    fun noDefault_noNetworksAvailable() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            connectivityRepository.defaultConnections.value = DefaultConnectionModel()
+
+            assertThat(latest?.secondaryLabel)
+                .isEqualTo(Text.Resource(R.string.quick_settings_networks_unavailable))
+            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_unavailable)
+        }
+
+    @Test
+    fun noDefault_networksAvailable() =
+        testScope.runTest {
+            // TODO(b/328419203): support [WifiInteractor.areNetworksAvailable]
+        }
+
+    @Test
+    fun wifiDefaultAndActive() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            val networkModel =
+                WifiNetworkModel.Active(
+                    networkId = 1,
+                    level = 4,
+                    ssid = "test ssid",
+                )
+            val wifiIcon =
+                WifiIcon.fromModel(model = networkModel, context = context, showHotspotInfo = true)
+                    as WifiIcon.Visible
+
+            connectivityRepository.setWifiConnected()
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(networkModel)
+
+            assertThat(latest?.secondaryTitle).isEqualTo("test ssid")
+            assertThat(latest?.secondaryLabel).isNull()
+            val expectedIcon =
+                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+
+            val actualIcon = latest?.icon
+            assertThat(actualIcon).isEqualTo(expectedIcon)
+            assertThat(latest?.iconId).isNull()
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo("$internet,test ssid")
+            val expectedSd = wifiIcon.contentDescription
+            assertThat(latest?.stateDescription).isEqualTo(expectedSd)
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotNone() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            val networkModel =
+                WifiNetworkModel.Active(
+                    networkId = 1,
+                    level = 4,
+                    ssid = "test ssid",
+                    hotspotDeviceType = WifiNetworkModel.HotspotDeviceType.NONE,
+                )
+
+            connectivityRepository.setWifiConnected()
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(networkModel)
+
+            val expectedIcon =
+                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .doesNotContain(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotTablet() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.TABLET)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_tablet)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotLaptop() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.LAPTOP)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_laptop)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotWatch() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.WATCH)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_watch)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotAuto() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.AUTO)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_auto)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotPhone() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.PHONE)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotUnknown() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.UNKNOWN)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndActive_hotspotInvalid() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.INVALID)
+
+            val expectedIcon =
+                Icon.Loaded(
+                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+                    null
+                )
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(
+                    context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+                )
+        }
+
+    @Test
+    fun wifiDefaultAndNotActive_noNetworksAvailable() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            val networkModel = WifiNetworkModel.Inactive
+
+            connectivityRepository.setWifiConnected(validated = false)
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(networkModel)
+            wifiRepository.wifiScanResults.value = emptyList()
+
+            assertThat(latest).isEqualTo(NOT_CONNECTED_NETWORKS_UNAVAILABLE)
+        }
+
+    @Test
+    fun wifiDefaultAndNotActive_networksAvailable() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            val networkModel = WifiNetworkModel.Inactive
+
+            connectivityRepository.setWifiConnected(validated = false)
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(networkModel)
+            wifiRepository.wifiScanResults.value = listOf(WifiScanEntry("test 1"))
+
+            assertThat(latest?.secondaryLabel).isNull()
+            assertThat(latest?.secondaryTitle)
+                .isEqualTo(context.getString(R.string.quick_settings_networks_available))
+            assertThat(latest?.icon).isNull()
+            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_available)
+            assertThat(latest?.stateDescription).isNull()
+            val expectedCd =
+                "$internet,${context.getString(R.string.quick_settings_networks_available)}"
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo(expectedCd)
+        }
+
+    @Test
+    fun mobileDefault_usesNetworkNameAndIcon() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+
+            connectivityRepository.setMobileConnected()
+            mobileConnectionsRepository.mobileIsDefault.value = true
+            mobileConnectionRepository.apply {
+                setAllLevels(3)
+                setAllRoaming(false)
+                networkName.value = NetworkNameModel.Default("test network")
+            }
+
+            assertThat(latest).isNotNull()
+            assertThat(latest?.secondaryTitle).isNotNull()
+            assertThat(latest?.secondaryTitle.toString()).contains("test network")
+            assertThat(latest?.secondaryLabel).isNull()
+            assertThat(latest?.icon).isInstanceOf(Icon.Loaded::class.java)
+            assertThat(latest?.iconId).isNull()
+            assertThat(latest?.stateDescription.loadContentDescription(context))
+                .isEqualTo(latest?.secondaryTitle.toString())
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo(internet)
+        }
+
+    @Test
+    fun ethernetDefault_validated_matchesInteractor() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            val ethernetIcon by collectLastValue(ethernetInteractor.icon)
+
+            connectivityRepository.setEthernetConnected(default = true, validated = true)
+
+            assertThat(latest?.secondaryLabel.loadText(context))
+                .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
+            assertThat(latest?.secondaryTitle).isNull()
+            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet_fully)
+            assertThat(latest?.icon).isNull()
+            assertThat(latest?.stateDescription).isNull()
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo(latest?.secondaryLabel.loadText(context))
+        }
+
+    @Test
+    fun ethernetDefault_notValidated_matchesInteractor() =
+        testScope.runTest {
+            val latest by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            val ethernetIcon by collectLastValue(ethernetInteractor.icon)
+
+            connectivityRepository.setEthernetConnected(default = true, validated = false)
+
+            assertThat(latest?.secondaryLabel.loadText(context))
+                .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
+            assertThat(latest?.secondaryTitle).isNull()
+            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet)
+            assertThat(latest?.icon).isNull()
+            assertThat(latest?.stateDescription).isNull()
+            assertThat(latest?.contentDescription.loadContentDescription(context))
+                .isEqualTo(latest?.secondaryLabel.loadText(context))
+        }
+
+    private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) {
+        val networkModel =
+            WifiNetworkModel.Active(
+                networkId = 1,
+                level = 4,
+                ssid = "test ssid",
+                hotspotDeviceType = hotspot,
+            )
+
+        connectivityRepository.setWifiConnected()
+        wifiRepository.setIsWifiDefault(true)
+        wifiRepository.setWifiNetwork(networkModel)
+    }
+
+    private companion object {
+        const val SUB_1_ID = 1
+
+        val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
+            InternetTileModel.Inactive(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..e1f3d97
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain.interactor
+
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.statusbar.connectivity.AccessPointController
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class InternetTileUserActionInteractorTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+
+    private lateinit var underTest: InternetTileUserActionInteractor
+
+    @Mock private lateinit var internetDialogManager: InternetDialogManager
+    @Mock private lateinit var controller: AccessPointController
+
+    @Before
+    fun setup() {
+        internetDialogManager = mock<InternetDialogManager>()
+        controller = mock<AccessPointController>()
+
+        underTest =
+            InternetTileUserActionInteractor(
+                kosmos.testScope.coroutineContext,
+                internetDialogManager,
+                controller,
+                inputHandler,
+            )
+    }
+
+    @Test
+    fun handleClickWhenActive() =
+        kosmos.testScope.runTest {
+            val input = InternetTileModel.Active()
+
+            underTest.handleInput(QSTileInputTestKtx.click(input))
+
+            verify(internetDialogManager).create(eq(true), anyBoolean(), anyBoolean(), nullable())
+        }
+
+    @Test
+    fun handleClickWhenInactive() =
+        kosmos.testScope.runTest {
+            val input = InternetTileModel.Inactive()
+
+            underTest.handleInput(QSTileInputTestKtx.click(input))
+
+            verify(internetDialogManager).create(eq(true), anyBoolean(), anyBoolean(), nullable())
+        }
+
+    @Test
+    fun handleLongClickWhenActive() =
+        kosmos.testScope.runTest {
+            val input = InternetTileModel.Active()
+
+            underTest.handleInput(QSTileInputTestKtx.longClick(input))
+
+            QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+                Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
+            }
+        }
+
+    @Test
+    fun handleLongClickWhenInactive() =
+        kosmos.testScope.runTest {
+            val input = InternetTileModel.Inactive()
+
+            underTest.handleInput(QSTileInputTestKtx.longClick(input))
+
+            QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+                Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
+            }
+        }
+}
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
index 61f69c0..f6042e4 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
@@ -128,7 +128,8 @@
         android:layout_marginStart="49dp"
         android:layout_marginEnd="49dp"
         android:overScrollMode="never"
-        android:layout_marginBottom="16dp">
+        android:layout_marginBottom="16dp"
+        android:scrollbars="none">
         <LinearLayout
             android:id="@+id/keyboard_shortcuts_container"
             android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e181d07..35f6a08 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -345,9 +345,6 @@
          the notification is not swiped enough to dismiss it. -->
     <bool name="config_showNotificationGear">true</bool>
 
-    <!-- Whether or not a background should be drawn behind a notification. -->
-    <bool name="config_drawNotificationBackground">false</bool>
-
     <!-- Whether or the notifications can be shown and dismissed with a drag. -->
     <bool name="config_enableNotificationShadeDrag">true</bool>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b713417..5dbdd18 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -75,6 +75,11 @@
     <!-- Battery saver notification dismiss action: Do not turn on battery saver. [CHAR LIMIT=NONE]-->
     <string name="battery_saver_dismiss_action">No thanks</string>
 
+    <!-- Secondary label for Battery Saver tile when Battery Saver is enabled. [CHAR LIMIT=20] -->
+    <string name="standard_battery_saver_text">Standard</string>
+    <!-- Secondary label for Battery Saver tile when Extreme Battery Saver is enabled. [CHAR LIMIT=20] -->
+    <string name="extreme_battery_saver_text">Extreme</string>
+
     <!-- Name of the button that links to the Settings app. [CHAR LIMIT=NONE] -->
 
     <!-- Name of the button that links to the Wifi settings screen. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index f28d405..8a2245d 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -404,7 +404,9 @@
                             if (nextAlarmMillis > 0) nextAlarmMillis else null,
                             SysuiR.string::status_bar_alarm.name
                         )
-                        .also { data -> clock?.run { events.onAlarmDataChanged(data) } }
+                        .also { data ->
+                            mainExecutor.execute { clock?.run { events.onAlarmDataChanged(data) } }
+                        }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 31698a3..01c2cc4 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -345,11 +345,25 @@
         }
     }
 
-    private TextView loadPercentView() {
+    private TextView inflatePercentView() {
         return (TextView) LayoutInflater.from(getContext())
                 .inflate(R.layout.battery_percentage_view, null);
     }
 
+    private void addPercentView(TextView inflatedPercentView) {
+        mBatteryPercentView = inflatedPercentView;
+
+        if (mPercentageStyleId != 0) { // Only set if specified as attribute
+            mBatteryPercentView.setTextAppearance(mPercentageStyleId);
+        }
+        float fontHeight = mBatteryPercentView.getPaint().getFontMetricsInt(null);
+        mBatteryPercentView.setLineHeight(TypedValue.COMPLEX_UNIT_PX, fontHeight);
+        if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
+        addView(mBatteryPercentView, new LayoutParams(
+                LayoutParams.WRAP_CONTENT,
+                (int) Math.ceil(fontHeight)));
+    }
+
     /**
      * Updates percent view by removing old one and reinflating if necessary
      */
@@ -388,7 +402,9 @@
             mBatteryEstimateFetcher.fetchBatteryTimeRemainingEstimate(
                     (String estimate) -> {
                         if (mBatteryPercentView == null) {
-                            mBatteryPercentView = loadPercentView();
+                            // Similar to the legacy behavior, inflate and add the view. We will
+                            // only use it for the estimate text
+                            addPercentView(inflatePercentView());
                         }
                         if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
                             mEstimateText = estimate;
@@ -401,6 +417,10 @@
                         }
                     });
         } else {
+            if (mBatteryPercentView != null) {
+                mEstimateText = null;
+                mBatteryPercentView.setText(null);
+            }
             updateContentDescription();
         }
     }
@@ -485,22 +505,19 @@
             return;
         }
 
-        if (mUnifiedBattery == null) {
-            return;
+        if (!mShowPercentAvailable || mUnifiedBattery == null) return;
+
+        boolean shouldShow = mShowPercentMode == MODE_ON || mShowPercentMode == MODE_ESTIMATE;
+        if (!mBatteryStateUnknown && !shouldShow && (mShowPercentMode != MODE_OFF)) {
+            // Slow case: fall back to the system setting
+            // TODO(b/140051051)
+            shouldShow = 0 != whitelistIpcs(() -> Settings.System
+                    .getIntForUser(getContext().getContentResolver(),
+                    SHOW_BATTERY_PERCENT, getContext().getResources().getBoolean(
+                    com.android.internal.R.bool.config_defaultBatteryPercentageSetting)
+                    ? 1 : 0, UserHandle.USER_CURRENT));
         }
 
-        // TODO(b/140051051)
-        final boolean systemSetting = 0 != whitelistIpcs(() -> Settings.System
-                .getIntForUser(getContext().getContentResolver(),
-                SHOW_BATTERY_PERCENT, getContext().getResources().getBoolean(
-                com.android.internal.R.bool.config_defaultBatteryPercentageSetting)
-                ? 1 : 0, UserHandle.USER_CURRENT));
-
-        boolean shouldShow =
-                (mShowPercentAvailable && systemSetting && mShowPercentMode != MODE_OFF)
-                        || mShowPercentMode == MODE_ON;
-        shouldShow = shouldShow && !mBatteryStateUnknown;
-
         setBatteryDrawableState(
                 new BatteryDrawableState(
                         mUnifiedBatteryState.getLevel(),
@@ -534,17 +551,8 @@
 
         if (shouldShow) {
             if (!showing) {
-                mBatteryPercentView = loadPercentView();
-                if (mPercentageStyleId != 0) { // Only set if specified as attribute
-                    mBatteryPercentView.setTextAppearance(mPercentageStyleId);
-                }
-                float fontHeight = mBatteryPercentView.getPaint().getFontMetricsInt(null);
-                mBatteryPercentView.setLineHeight(TypedValue.COMPLEX_UNIT_PX, fontHeight);
-                if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
+                addPercentView(inflatePercentView());
                 updatePercentText();
-                addView(mBatteryPercentView, new LayoutParams(
-                        LayoutParams.WRAP_CONTENT,
-                        (int) Math.ceil(fontHeight)));
             }
         } else {
             if (showing) {
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
index 8c9ae62..10a0d95 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
@@ -1,9 +1,21 @@
 package com.android.systemui.battery
 
+import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.BatterySaverTile
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.impl.battery.domain.interactor.BatterySaverTileDataInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.interactor.BatterySaverTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.qs.tiles.impl.battery.ui.BatterySaverTileMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.res.R
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 import dagger.multibindings.IntoMap
 import dagger.multibindings.StringKey
 
@@ -15,4 +27,39 @@
     @IntoMap
     @StringKey(BatterySaverTile.TILE_SPEC)
     fun bindBatterySaverTile(batterySaverTile: BatterySaverTile): QSTileImpl<*>
+
+    companion object {
+        private const val BATTERY_SAVER_TILE_SPEC = "battery"
+
+        @Provides
+        @IntoMap
+        @StringKey(BATTERY_SAVER_TILE_SPEC)
+        fun provideBatterySaverTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(BATTERY_SAVER_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.qs_battery_saver_icon_off,
+                        labelRes = R.string.battery_detail_switch_title,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /** Inject BatterySaverTile into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(BATTERY_SAVER_TILE_SPEC)
+        fun provideBatterySaverTileViewModel(
+            factory: QSTileViewModelFactory.Static<BatterySaverTileModel>,
+            mapper: BatterySaverTileMapper,
+            stateInteractor: BatterySaverTileDataInteractor,
+            userActionInteractor: BatterySaverTileUserActionInteractor
+        ): QSTileViewModel =
+            factory.create(
+                TileSpec.create(BATTERY_SAVER_TILE_SPEC),
+                userActionInteractor,
+                stateInteractor,
+                mapper,
+            )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryAttributionDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryAttributionDrawable.kt
index 1b8495a..f3652b8 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryAttributionDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryAttributionDrawable.kt
@@ -23,6 +23,7 @@
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.DrawableWrapper
 import android.view.Gravity
+import kotlin.math.ceil
 import kotlin.math.min
 import kotlin.math.roundToInt
 
@@ -36,7 +37,7 @@
  */
 @Suppress("RtlHardcoded")
 class BatteryAttributionDrawable(dr: Drawable?) : DrawableWrapper(dr) {
-    /** One of [CENTER, LEFT]. Note that RTL is handled in the parent */
+    /** One of [CENTER, LEFT]. Note that number text does not RTL. */
     var gravity = Gravity.CENTER
         set(value) {
             field = value
@@ -67,8 +68,8 @@
             dr.setBounds(
                 bounds.left,
                 bounds.top,
-                (bounds.left + dw).roundToInt(),
-                (bounds.top + dh).roundToInt()
+                ceil(bounds.left + dw).toInt(),
+                ceil(bounds.top + dh).toInt()
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt
index 6d32067..5e34d29 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt
@@ -26,6 +26,7 @@
 import android.graphics.Rect
 import android.graphics.RectF
 import android.graphics.drawable.Drawable
+import android.view.View
 import com.android.systemui.battery.unified.BatteryLayersDrawable.Companion.Metrics
 import kotlin.math.floor
 import kotlin.math.roundToInt
@@ -103,6 +104,11 @@
         // saveLayer is needed here so we don't clip the other layers of our drawable
         canvas.saveLayer(null, null)
 
+        // Fill from the opposite direction in rtl mode
+        if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+            canvas.scale(-1f, 1f, bounds.width() / 2f, bounds.height() / 2f)
+        }
+
         // We need to use 3 draw commands:
         // 1. Clip to the current level
         // 2. Clip anything outside of the path
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryLayersDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryLayersDrawable.kt
index 199dd1f..706b9ec 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryLayersDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryLayersDrawable.kt
@@ -26,7 +26,10 @@
 import android.graphics.drawable.LayerDrawable
 import android.util.PathParser
 import android.view.Gravity
+import android.view.View
 import com.android.systemui.res.R
+import kotlin.math.ceil
+import kotlin.math.floor
 import kotlin.math.roundToInt
 
 /**
@@ -69,8 +72,11 @@
 ) : LayerDrawable(arrayOf(frameBg, frame, fill, textOnly, spaceSharingText, attribution)) {
 
     private val scaleMatrix = Matrix().also { it.setScale(1f, 1f) }
-    private val scaledAttrFullCanvas = RectF(Metrics.AttrFullCanvas)
-    private val scaledAttrRightCanvas = RectF(Metrics.AttrRightCanvas)
+
+    private val attrFullCanvas = RectF()
+    private val attrRightCanvas = RectF()
+    private val scaledAttrFullCanvas = RectF()
+    private val scaledAttrRightCanvas = RectF()
 
     var batteryState = batteryState
         set(value) {
@@ -88,6 +94,12 @@
             updateColors(batteryState.showErrorState, value)
         }
 
+    init {
+        isAutoMirrored = true
+        // Initialize the canvas rects since they are not static
+        setAttrRects(layoutDirection == View.LAYOUT_DIRECTION_RTL)
+    }
+
     private fun handleUpdateState(old: BatteryDrawableState, new: BatteryDrawableState) {
         if (new.showErrorState != old.showErrorState) {
             updateColors(new.showErrorState, colors)
@@ -144,9 +156,42 @@
             bounds.height() / Metrics.ViewportHeight
         )
 
-        // Scale the attribution bounds
-        scaleMatrix.mapRect(scaledAttrFullCanvas, Metrics.AttrFullCanvas)
-        scaleMatrix.mapRect(scaledAttrRightCanvas, Metrics.AttrRightCanvas)
+        scaleAttributionBounds()
+    }
+
+    override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
+        setAttrRects(layoutDirection == View.LAYOUT_DIRECTION_RTL)
+        scaleAttributionBounds()
+
+        return super.onLayoutDirectionChanged(layoutDirection)
+    }
+
+    private fun setAttrRects(rtl: Boolean) {
+        // Local refs make the math easier to parse
+        val full = Metrics.AttrFullCanvasInsets
+        val side = Metrics.AttrRightCanvasInsets
+        val sideRtl = Metrics.AttrRightCanvasInsetsRtl
+        val vh = Metrics.ViewportHeight
+        val vw = Metrics.ViewportWidth
+
+        attrFullCanvas.set(
+            if (rtl) full.right else full.left,
+            full.top,
+            vw - if (rtl) full.left else full.right,
+            vh - full.bottom,
+        )
+        attrRightCanvas.set(
+            if (rtl) sideRtl.left else side.left,
+            side.top,
+            vw - (if (rtl) sideRtl.right else side.right),
+            vh - side.bottom,
+        )
+    }
+
+    /** If bounds (i.e., scale), or RTL properties change, we have to recalculate the attr bounds */
+    private fun scaleAttributionBounds() {
+        scaleMatrix.mapRect(scaledAttrFullCanvas, attrFullCanvas)
+        scaleMatrix.mapRect(scaledAttrRightCanvas, attrRightCanvas)
     }
 
     override fun draw(canvas: Canvas) {
@@ -163,13 +208,14 @@
         if (batteryState.showPercent && batteryState.attribution != null) {
             // 4a. percent & attribution. Implies space-sharing
 
-            // Configure the attribute to draw in a smaller bounding box and align left
+            // Configure the attribute to draw in a smaller bounding box and align left and use
+            // floor/ceil math to make sure we get every available pixel
             attribution.gravity = Gravity.LEFT
             attribution.setBounds(
-                scaledAttrRightCanvas.left.roundToInt(),
-                scaledAttrRightCanvas.top.roundToInt(),
-                scaledAttrRightCanvas.right.roundToInt(),
-                scaledAttrRightCanvas.bottom.roundToInt(),
+                floor(scaledAttrRightCanvas.left).toInt(),
+                floor(scaledAttrRightCanvas.top).toInt(),
+                ceil(scaledAttrRightCanvas.right).toInt(),
+                ceil(scaledAttrRightCanvas.bottom).toInt(),
             )
             attribution.draw(canvas)
 
@@ -196,16 +242,44 @@
      */
     override fun setAlpha(alpha: Int) {}
 
+    /**
+     * Interface that describes relevant top-level metrics for the proper rendering of this icon.
+     * The overall canvas is defined as ViewportWidth x ViewportHeight, which is hard coded to 24x14
+     * points.
+     *
+     * The attr canvas insets are rect inset definitions. That is, they are defined as l,t,r,b
+     * points from the nearest edge. Note that for RTL, we don't actually flip the text since
+     * numbers do not reverse for RTL locales.
+     */
     interface M {
         val ViewportWidth: Float
         val ViewportHeight: Float
 
-        // Bounds, oriented in the above viewport, where we will fit-center and center-align
-        // an attribution that is the sole foreground element
-        val AttrFullCanvas: RectF
-        // Bounds, oriented in the above viewport, where we will fit-center and left-align
-        // an attribution that is sharing space with the percent text of the drawable
-        val AttrRightCanvas: RectF
+        /**
+         * Insets, oriented in the above viewport in LTR, that define the full canvas for a single
+         * foreground element. The element will be fit-center and center-aligned on this canvas
+         *
+         * 18x8 point size
+         */
+        val AttrFullCanvasInsets: RectF
+
+        /**
+         * Insets, oriented in the above viewport in LTR, that define the partial canvas for a
+         * foreground element that shares space with the percent text. The element will be
+         * fit-center and left-aligned on this canvas.
+         *
+         * 6x6 point size
+         */
+        val AttrRightCanvasInsets: RectF
+
+        /**
+         * Insets, oriented in the above viewport in RTL, that define the partial canvas for a
+         * foreground element that shares space with the percent text. The element will be
+         * fit-center and left-aligned on this canvas.
+         *
+         * 6x6 point size
+         */
+        val AttrRightCanvasInsetsRtl: RectF
     }
 
     companion object {
@@ -220,20 +294,9 @@
                 override val ViewportWidth: Float = 24f
                 override val ViewportHeight: Float = 14f
 
-                /**
-                 * Bounds, oriented in the above viewport, where we will fit-center and center-align
-                 * an attribution that is the sole foreground element
-                 *
-                 * 18x8 point size
-                 */
-                override val AttrFullCanvas: RectF = RectF(4f, 3f, 22f, 11f)
-                /**
-                 * Bounds, oriented in the above viewport, where we will fit-center and left-align
-                 * an attribution that is sharing space with the percent text of the drawable
-                 *
-                 * 6x6 point size
-                 */
-                override val AttrRightCanvas: RectF = RectF(16f, 4f, 22f, 10f)
+                override val AttrFullCanvasInsets = RectF(4f, 3f, 2f, 3f)
+                override val AttrRightCanvasInsets = RectF(16f, 4f, 2f, 4f)
+                override val AttrRightCanvasInsetsRtl = RectF(14f, 4f, 4f, 4f)
             }
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryPercentTextOnlyDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryPercentTextOnlyDrawable.kt
index 123d6ba..aa0e373 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryPercentTextOnlyDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryPercentTextOnlyDrawable.kt
@@ -23,6 +23,7 @@
 import android.graphics.Rect
 import android.graphics.Typeface
 import android.graphics.drawable.Drawable
+import android.view.View
 import com.android.systemui.battery.unified.BatteryLayersDrawable.Companion.Metrics
 
 /**
@@ -71,6 +72,7 @@
     }
 
     override fun draw(canvas: Canvas) {
+        val rtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
         val totalAvailableHeight = CanvasHeight * vScale
 
         // Distribute the vertical whitespace around the text. This is a simplified version of
@@ -81,11 +83,12 @@
         val totalAvailableWidth = CanvasWidth * hScale
         val textWidth = textPaint.measureText(percentText)
         val offsetX = (totalAvailableWidth - textWidth) / 2
+        val startOffset = if (rtl) ViewportInsetRight else ViewportInsetLeft
 
         // Draw the text centered in the available area
         canvas.drawText(
             percentText,
-            (ViewportInsetLeft * hScale) + offsetX,
+            (startOffset * hScale) + offsetX,
             (ViewportInsetTop * vScale) + offsetY,
             textPaint
         )
diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatterySpaceSharingPercentTextDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatterySpaceSharingPercentTextDrawable.kt
index 0c418b9..3b4c779 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatterySpaceSharingPercentTextDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatterySpaceSharingPercentTextDrawable.kt
@@ -23,6 +23,7 @@
 import android.graphics.Rect
 import android.graphics.Typeface
 import android.graphics.drawable.Drawable
+import android.view.View
 import com.android.systemui.battery.unified.BatteryLayersDrawable.Companion.Metrics
 
 /**
@@ -94,6 +95,7 @@
     }
 
     override fun draw(canvas: Canvas) {
+        val rtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
         val totalAvailableHeight = CanvasHeight * vScale
 
         // Distribute the vertical whitespace around the text. This is a simplified version of
@@ -107,7 +109,7 @@
 
         canvas.drawText(
             percentText,
-            (ViewportInsetLeft * hScale) + offsetX,
+            ((if (rtl) ViewportInsetLeftRtl else ViewportInsetLeft) * hScale) + offsetX,
             (ViewportInsetTop * vScale) + offsetY,
             textPaint
         )
@@ -128,6 +130,7 @@
 
     companion object {
         private const val ViewportInsetLeft = 4f
+        private const val ViewportInsetLeftRtl = 2f
         private const val ViewportInsetTop = 2f
 
         private const val CanvasWidth = 12f
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
index 6af0fa0..66aeda6 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialogDelegate.java
@@ -42,7 +42,7 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.media.controls.util.MediaDataUtils;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
@@ -69,7 +69,7 @@
 
     private final Context mContext;
     private final UiEventLogger mUiEventLogger;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final MediaOutputDialogManager mMediaOutputDialogManager;
     private final LocalBluetoothManager mLocalBluetoothManager;
     private final BroadcastSender mBroadcastSender;
     private final SystemUIDialog.Factory mSystemUIDialogFactory;
@@ -157,7 +157,7 @@
     @AssistedInject
     BroadcastDialogDelegate(
             Context context,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaOutputDialogManager mediaOutputDialogManager,
             @Nullable LocalBluetoothManager localBluetoothManager,
             UiEventLogger uiEventLogger,
             @Background Executor bgExecutor,
@@ -166,7 +166,7 @@
             @Assisted(CURRENT_BROADCAST_APP) String currentBroadcastApp,
             @Assisted(OUTPUT_PKG_NAME) String outputPkgName) {
         mContext = context;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaOutputDialogManager = mediaOutputDialogManager;
         mLocalBluetoothManager = localBluetoothManager;
         mSystemUIDialogFactory = systemUIDialogFactory;
         mCurrentBroadcastApp = currentBroadcastApp;
@@ -218,7 +218,7 @@
                 R.string.bt_le_audio_broadcast_dialog_switch_app, switchBroadcastApp), null);
         mSwitchBroadcast.setOnClickListener((view) -> startSwitchBroadcast());
         changeOutput.setOnClickListener((view) -> {
-            mMediaOutputDialogFactory.create(mOutputPackageName, true, null);
+            mMediaOutputDialogManager.createAndShow(mOutputPackageName, true, null);
             dialog.dismiss();
         });
         cancelBtn.setOnClickListener((view) -> {
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 35b27aa..fc9a7df 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
@@ -21,7 +21,6 @@
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
@@ -47,7 +46,6 @@
 import kotlinx.coroutines.launch
 
 /** The default view model used for showing the communal hub. */
-@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class CommunalViewModel
 @Inject
@@ -56,7 +54,6 @@
     private val communalInteractor: CommunalInteractor,
     tutorialInteractor: CommunalTutorialInteractor,
     shadeInteractor: ShadeInteractor,
-    deviceEntryInteractor: DeviceEntryInteractor,
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
     @CommunalLog logBuffer: LogBuffer,
 ) : BaseCommunalViewModel(communalInteractor, mediaHost) {
@@ -90,8 +87,6 @@
     /** Whether touches should be disabled in communal */
     val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded)
 
-    val deviceUnlocked: Flow<Boolean> = deviceEntryInteractor.isUnlocked
-
     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.
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
index 79455eb..289dbd9 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
@@ -24,9 +24,12 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.shared.model.BiometricMessage
 import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -60,17 +63,23 @@
     private val context: Context,
     activityStarter: ActivityStarter,
     powerInteractor: PowerInteractor,
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
     private val keyguardOccludedByApp: Flow<Boolean> =
-        combine(
-                keyguardInteractor.isKeyguardOccluded,
-                keyguardInteractor.isKeyguardShowing,
-                primaryBouncerInteractor.isShowing,
-                alternateBouncerInteractor.isVisible,
-            ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible ->
-                occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible
-            }
-            .distinctUntilChanged()
+        if (KeyguardWmStateRefactor.isEnabled) {
+            keyguardTransitionInteractor.currentKeyguardState.map { it == KeyguardState.OCCLUDED }
+        } else {
+            combine(
+                    keyguardInteractor.isKeyguardOccluded,
+                    keyguardInteractor.isKeyguardShowing,
+                    primaryBouncerInteractor.isShowing,
+                    alternateBouncerInteractor.isVisible,
+                ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible ->
+                    occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible
+                }
+                .distinctUntilChanged()
+        }
+
     private val fingerprintUnlockSuccessEvents: Flow<Unit> =
         fingerprintAuthRepository.authenticationStatus
             .ifKeyguardOccludedByApp()
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
index 931a869..ed82278 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
@@ -163,6 +163,6 @@
     }
 
     companion object {
-        const val KEY_UP_TIMEOUT = 100L
+        const val KEY_UP_TIMEOUT = 60L
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 2e233d8..3134e35 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -289,6 +289,8 @@
         };
     }
 
+    private final WindowManagerOcclusionManager mWmOcclusionManager;
+
     @Inject
     public KeyguardService(
             KeyguardViewMediator keyguardViewMediator,
@@ -302,7 +304,8 @@
             KeyguardSurfaceBehindParamsApplier keyguardSurfaceBehindAnimator,
             @Application CoroutineScope scope,
             FeatureFlags featureFlags,
-            PowerInteractor powerInteractor) {
+            PowerInteractor powerInteractor,
+            WindowManagerOcclusionManager windowManagerOcclusionManager) {
         super();
         mKeyguardViewMediator = keyguardViewMediator;
         mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
@@ -323,6 +326,8 @@
                     keyguardSurfaceBehindAnimator,
                     scope);
         }
+
+        mWmOcclusionManager = windowManagerOcclusionManager;
     }
 
     @Override
@@ -414,7 +419,11 @@
 
             Trace.beginSection("KeyguardService.mBinder#setOccluded");
             checkPermission();
-            mKeyguardViewMediator.setOccluded(isOccluded, animate);
+            if (!KeyguardWmStateRefactor.isEnabled()) {
+                mKeyguardViewMediator.setOccluded(isOccluded, animate);
+            } else {
+                mWmOcclusionManager.onKeyguardServiceSetOccluded(isOccluded);
+            }
             Trace.endSection();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 6d917bb..a37397d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -1365,7 +1365,9 @@
 
     private final Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager;
 
+    private WindowManagerOcclusionManager mWmOcclusionManager;
     /**
+
      * Injected constructor. See {@link KeyguardModule}.
      */
     public KeyguardViewMediator(
@@ -1411,7 +1413,8 @@
             SystemPropertiesHelper systemPropertiesHelper,
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
-            KeyguardInteractor keyguardInteractor) {
+            KeyguardInteractor keyguardInteractor,
+            WindowManagerOcclusionManager wmOcclusionManager) {
         mContext = context;
         mUserTracker = userTracker;
         mFalsingCollector = falsingCollector;
@@ -1486,6 +1489,8 @@
 
         mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard");
         mShowKeyguardWakeLock.setReferenceCounted(false);
+
+        mWmOcclusionManager = wmOcclusionManager;
     }
 
     public void userActivity() {
@@ -2103,15 +2108,27 @@
     }
 
     public IRemoteAnimationRunner getOccludeAnimationRunner() {
-        return validatingRemoteAnimationRunner(mOccludeAnimationRunner);
+        if (KeyguardWmStateRefactor.isEnabled()) {
+            return validatingRemoteAnimationRunner(mWmOcclusionManager.getOccludeAnimationRunner());
+        } else {
+            return validatingRemoteAnimationRunner(mOccludeAnimationRunner);
+        }
     }
 
+    /**
+     * TODO(b/326464548): Move this to WindowManagerOcclusionManager
+     */
     public IRemoteAnimationRunner getOccludeByDreamAnimationRunner() {
         return validatingRemoteAnimationRunner(mOccludeByDreamAnimationRunner);
     }
 
     public IRemoteAnimationRunner getUnoccludeAnimationRunner() {
-        return validatingRemoteAnimationRunner(mUnoccludeAnimationRunner);
+        if (KeyguardWmStateRefactor.isEnabled()) {
+            return validatingRemoteAnimationRunner(
+                    mWmOcclusionManager.getUnoccludeAnimationRunner());
+        } else {
+            return validatingRemoteAnimationRunner(mUnoccludeAnimationRunner);
+        }
     }
 
     public boolean isHiding() {
@@ -2145,8 +2162,10 @@
 
             if (mOccluded != isOccluded) {
                 mOccluded = isOccluded;
-                mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate
-                        && mDeviceInteractive);
+                if (!KeyguardWmStateRefactor.isEnabled()) {
+                    mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate
+                            && mDeviceInteractive);
+                }
                 adjustStatusBarLocked();
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
index 8ebcece..00f5002 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
@@ -140,8 +140,14 @@
         nonApps: Array<RemoteAnimationTarget>,
         finishedCallback: IRemoteAnimationFinishedCallback
     ) {
-        goingAwayRemoteAnimationFinishedCallback = finishedCallback
-        keyguardSurfaceBehindAnimator.applyParamsToSurface(apps[0])
+        if (apps.isNotEmpty()) {
+            goingAwayRemoteAnimationFinishedCallback = finishedCallback
+            keyguardSurfaceBehindAnimator.applyParamsToSurface(apps[0])
+        } else {
+            // Nothing to do here if we have no apps, end the animation, which will cancel it and WM
+            // will make *something* visible.
+            finishedCallback.onAnimationFinished()
+        }
     }
 
     fun onKeyguardGoingAwayRemoteAnimationCancelled() {
@@ -174,13 +180,19 @@
      * if so, true should be the right choice.
      */
     private fun setWmLockscreenState(
-            lockscreenShowing: Boolean = this.isLockscreenShowing ?: true.also {
-                Log.d(TAG, "Using isLockscreenShowing=true default in setWmLockscreenState, " +
-                        "because setAodVisible was called before the first setLockscreenShown " +
-                        "call during boot. This is not typical, but is theoretically possible. " +
-                        "If you're investigating the lockscreen showing unexpectedly, start here.")
-            },
-            aodVisible: Boolean = this.isAodVisible
+        lockscreenShowing: Boolean =
+            this.isLockscreenShowing
+                ?: true.also {
+                    Log.d(
+                        TAG,
+                        "Using isLockscreenShowing=true default in setWmLockscreenState, " +
+                            "because setAodVisible was called before the first " +
+                            "setLockscreenShown call during boot. This is not typical, but is " +
+                            "theoretically possible. If you're investigating the lockscreen " +
+                            "showing unexpectedly, start here."
+                    )
+                },
+        aodVisible: Boolean = this.isAodVisible
     ) {
         Log.d(
             TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt
new file mode 100644
index 0000000..aab90c3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt
@@ -0,0 +1,327 @@
+/*
+ * 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
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Matrix
+import android.os.RemoteException
+import android.util.Log
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationTarget
+import android.view.SyncRtSurfaceTransactionApplier
+import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+import androidx.annotation.VisibleForTesting
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.keyguard.KeyguardViewController
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.TransitionAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.res.R
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private val UNOCCLUDE_ANIMATION_DURATION = 250
+private val UNOCCLUDE_TRANSLATE_DISTANCE_PERCENT = 0.1f
+
+/**
+ * Keeps track of Window Manager's occlusion state and RemoteAnimations related to changes in
+ * occlusion state. Occlusion is when a [FLAG_SHOW_WHEN_LOCKED] activity is displaying over the
+ * lockscreen - we're still locked, but the user can interact with the activity.
+ *
+ * Typical "occlusion" use cases include launching the camera over the lockscreen, tapping a quick
+ * affordance to bring up Google Pay/Wallet/whatever it's called by the time you're reading this,
+ * and Maps Navigation.
+ *
+ * Window Manager considers the keyguard to be 'occluded' whenever a [FLAG_SHOW_WHEN_LOCKED]
+ * activity is on top of the task stack, even if the device is unlocked and the keyguard is not
+ * visible. System UI considers the keyguard to be [KeyguardState.OCCLUDED] only when we're on the
+ * keyguard and an activity is displaying over it.
+ *
+ * For all System UI use cases, you should use [KeyguardTransitionInteractor] to determine if we're
+ * in the [KeyguardState.OCCLUDED] state and react accordingly. If you are sure that you need to
+ * check whether Window Manager considers OCCLUDED=true even though the lockscreen is not showing,
+ * use [KeyguardShowWhenLockedActivityInteractor.isShowWhenLockedActivityOnTop] in combination with
+ * [KeyguardTransitionInteractor] state.
+ *
+ * This is a very sensitive piece of state that has caused many headaches in the past. Please be
+ * careful.
+ */
+@SysUISingleton
+class WindowManagerOcclusionManager
+@Inject
+constructor(
+    val keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    val activityTransitionAnimator: ActivityTransitionAnimator,
+    val keyguardViewController: dagger.Lazy<KeyguardViewController>,
+    val powerInteractor: PowerInteractor,
+    val context: Context,
+    val interactionJankMonitor: InteractionJankMonitor,
+    @Main executor: Executor,
+    val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+    val occlusionInteractor: KeyguardOcclusionInteractor,
+) {
+    val powerButtonY =
+        context.resources.getDimensionPixelSize(
+            R.dimen.physical_power_button_center_screen_location_y
+        )
+    val windowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+
+    var occludeAnimationFinishedCallback: IRemoteAnimationFinishedCallback? = null
+
+    /**
+     * Animation runner provided to WindowManager, which will be used if an occluding activity is
+     * launched and Window Manager wants us to animate it in. This is used as a signal that we are
+     * now occluded, and should update our state accordingly.
+     */
+    val occludeAnimationRunner: IRemoteAnimationRunner =
+        object : IRemoteAnimationRunner.Stub() {
+            override fun onAnimationStart(
+                transit: Int,
+                apps: Array<RemoteAnimationTarget>,
+                wallpapers: Array<RemoteAnimationTarget>,
+                nonApps: Array<RemoteAnimationTarget>,
+                finishedCallback: IRemoteAnimationFinishedCallback?
+            ) {
+                Log.d(TAG, "occludeAnimationRunner#onAnimationStart")
+                // Wrap the callback so that it's guaranteed to be nulled out once called.
+                occludeAnimationFinishedCallback =
+                    object : IRemoteAnimationFinishedCallback.Stub() {
+                        override fun onAnimationFinished() {
+                            finishedCallback?.onAnimationFinished()
+                            occludeAnimationFinishedCallback = null
+                        }
+                    }
+                keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+                    showWhenLockedActivityOnTop = true,
+                    taskInfo = apps.firstOrNull()?.taskInfo,
+                )
+                activityTransitionAnimator
+                    .createRunner(occludeAnimationController)
+                    .onAnimationStart(
+                        transit,
+                        apps,
+                        wallpapers,
+                        nonApps,
+                        occludeAnimationFinishedCallback,
+                    )
+            }
+
+            override fun onAnimationCancelled() {
+                Log.d(TAG, "occludeAnimationRunner#onAnimationCancelled")
+            }
+        }
+
+    var unoccludeAnimationFinishedCallback: IRemoteAnimationFinishedCallback? = null
+
+    /**
+     * Animation runner provided to WindowManager, which will be used if an occluding activity is
+     * finished and Window Manager wants us to animate it out. This is used as a signal that we are
+     * no longer occluded, and should update our state accordingly.
+     *
+     * TODO(b/326464548): Restore dream specific animation.
+     */
+    val unoccludeAnimationRunner: IRemoteAnimationRunner =
+        object : IRemoteAnimationRunner.Stub() {
+            var unoccludeAnimator: ValueAnimator? = null
+            val unoccludeMatrix = Matrix()
+
+            /** TODO(b/326470033): Extract this logic into ViewModels. */
+            override fun onAnimationStart(
+                transit: Int,
+                apps: Array<RemoteAnimationTarget>,
+                wallpapers: Array<RemoteAnimationTarget>,
+                nonApps: Array<RemoteAnimationTarget>,
+                finishedCallback: IRemoteAnimationFinishedCallback?
+            ) {
+                Log.d(TAG, "unoccludeAnimationRunner#onAnimationStart")
+                // Wrap the callback so that it's guaranteed to be nulled out once called.
+                unoccludeAnimationFinishedCallback =
+                    object : IRemoteAnimationFinishedCallback.Stub() {
+                        override fun onAnimationFinished() {
+                            finishedCallback?.onAnimationFinished()
+                            unoccludeAnimationFinishedCallback = null
+                        }
+                    }
+                keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+                    showWhenLockedActivityOnTop = false,
+                    taskInfo = apps.firstOrNull()?.taskInfo,
+                )
+                interactionJankMonitor.begin(
+                    createInteractionJankMonitorConf(
+                        InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION,
+                        "UNOCCLUDE"
+                    )
+                )
+                if (apps.isEmpty()) {
+                    Log.d(
+                        TAG,
+                        "No apps provided to unocclude runner; " +
+                            "skipping animation and unoccluding."
+                    )
+                    unoccludeAnimationFinishedCallback?.onAnimationFinished()
+                    return
+                }
+                val target = apps[0]
+                val localView: View = keyguardViewController.get().getViewRootImpl().getView()
+                val applier = SyncRtSurfaceTransactionApplier(localView)
+                // TODO(
+                executor.execute {
+                    unoccludeAnimator?.cancel()
+                    unoccludeAnimator =
+                        ValueAnimator.ofFloat(1f, 0f).apply {
+                            duration = UNOCCLUDE_ANIMATION_DURATION.toLong()
+                            interpolator = Interpolators.TOUCH_RESPONSE
+                            addUpdateListener { animation: ValueAnimator ->
+                                val animatedValue = animation.animatedValue as Float
+                                val surfaceHeight: Float =
+                                    target.screenSpaceBounds.height().toFloat()
+
+                                unoccludeMatrix.setTranslate(
+                                    0f,
+                                    (1f - animatedValue) *
+                                        surfaceHeight *
+                                        UNOCCLUDE_TRANSLATE_DISTANCE_PERCENT
+                                )
+
+                                SurfaceParams.Builder(target.leash)
+                                    .withAlpha(animatedValue)
+                                    .withMatrix(unoccludeMatrix)
+                                    .withCornerRadius(windowCornerRadius)
+                                    .build()
+                                    .also { applier.scheduleApply(it) }
+                            }
+                            addListener(
+                                object : AnimatorListenerAdapter() {
+                                    override fun onAnimationEnd(animation: Animator) {
+                                        try {
+                                            unoccludeAnimationFinishedCallback
+                                                ?.onAnimationFinished()
+                                            unoccludeAnimator = null
+                                            interactionJankMonitor.end(
+                                                InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION
+                                            )
+                                        } catch (e: RemoteException) {
+                                            e.printStackTrace()
+                                        }
+                                    }
+                                }
+                            )
+                            start()
+                        }
+                }
+            }
+
+            override fun onAnimationCancelled() {
+                Log.d(TAG, "unoccludeAnimationRunner#onAnimationCancelled")
+                context.mainExecutor.execute { unoccludeAnimator?.cancel() }
+                Log.d(TAG, "Unocclude animation cancelled.")
+                interactionJankMonitor.cancel(InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION)
+            }
+        }
+
+    /**
+     * Called when Window Manager tells the KeyguardService directly that we're occluded or not
+     * occluded, without starting an occlude/unocclude remote animation. This happens if occlusion
+     * state changes without an animation (such as if a SHOW_WHEN_LOCKED activity is launched while
+     * we're unlocked), or if an animation has been cancelled/interrupted and Window Manager wants
+     * to make sure that we're in the correct state.
+     */
+    fun onKeyguardServiceSetOccluded(occluded: Boolean) {
+        Log.d(TAG, "#onKeyguardServiceSetOccluded($occluded)")
+        keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(occluded)
+    }
+
+    @VisibleForTesting
+    val occludeAnimationController: ActivityTransitionAnimator.Controller =
+        object : ActivityTransitionAnimator.Controller {
+
+            override var transitionContainer: ViewGroup
+                get() = keyguardViewController.get().getViewRootImpl().view as ViewGroup
+                set(_) {
+                    // Should never be set.
+                }
+
+            /** TODO(b/326470033): Extract this logic into ViewModels. */
+            override fun createAnimatorState(): TransitionAnimator.State {
+                val fullWidth = transitionContainer.width
+                val fullHeight = transitionContainer.height
+
+                if (
+                    keyguardOcclusionInteractor.showWhenLockedActivityLaunchedFromPowerGesture.value
+                ) {
+                    val initialHeight = fullHeight / 3f
+                    val initialWidth = fullWidth / 3f
+
+                    // Start the animation near the power button, at one-third size, since the
+                    // camera was launched from the power button.
+                    return TransitionAnimator.State(
+                        top = (powerButtonY - initialHeight / 2f).toInt(),
+                        bottom = (powerButtonY + initialHeight / 2f).toInt(),
+                        left = (fullWidth - initialWidth).toInt(),
+                        right = fullWidth,
+                        topCornerRadius = windowCornerRadius,
+                        bottomCornerRadius = windowCornerRadius,
+                    )
+                } else {
+                    val initialHeight = fullHeight / 2f
+                    val initialWidth = fullWidth / 2f
+
+                    // Start the animation in the center of the screen, scaled down to half
+                    // size.
+                    return TransitionAnimator.State(
+                        top = (fullHeight - initialHeight).toInt() / 2,
+                        bottom = (initialHeight + (fullHeight - initialHeight) / 2).toInt(),
+                        left = (fullWidth - initialWidth).toInt() / 2,
+                        right = (initialWidth + (fullWidth - initialWidth) / 2).toInt(),
+                        topCornerRadius = windowCornerRadius,
+                        bottomCornerRadius = windowCornerRadius,
+                    )
+                }
+            }
+        }
+
+    private fun createInteractionJankMonitorConf(
+        cuj: Int,
+        tag: String?
+    ): InteractionJankMonitor.Configuration.Builder {
+        val builder =
+            InteractionJankMonitor.Configuration.Builder.withView(
+                cuj,
+                keyguardViewController.get().getViewRootImpl().view
+            )
+        return if (tag != null) builder.setTag(tag) else builder
+    }
+
+    companion object {
+        val TAG = "WindowManagerOcclusion"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 968c3e3..5306645 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -50,6 +50,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager;
+import com.android.systemui.keyguard.WindowManagerOcclusionManager;
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
@@ -163,7 +164,8 @@
             SystemPropertiesHelper systemPropertiesHelper,
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
-            KeyguardInteractor keyguardInteractor) {
+            KeyguardInteractor keyguardInteractor,
+            WindowManagerOcclusionManager windowManagerOcclusionManager) {
         return new KeyguardViewMediator(
                 context,
                 uiEventLogger,
@@ -209,7 +211,8 @@
                 systemPropertiesHelper,
                 wmLockscreenVisibilityManager,
                 selectedUserInteractor,
-                keyguardInteractor);
+                keyguardInteractor,
+                windowManagerOcclusionManager);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt
new file mode 100644
index 0000000..e3654b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/**
+ * Information about the SHOW_WHEN_LOCKED activity that is either newly on top of the task stack, or
+ * newly not on top of the task stack.
+ */
+data class ShowWhenLockedActivityInfo(
+    /** Whether the activity is on top. If not, we're unoccluding and will be animating it out. */
+    val isOnTop: Boolean,
+
+    /**
+     * Information about the activity, which we use for transition internals and also to customize
+     * animations.
+     */
+    val taskInfo: RunningTaskInfo? = null
+) {
+    fun isDream(): Boolean {
+        return taskInfo?.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM
+    }
+}
+
+/**
+ * Maintains state about "occluding" activities - activities with FLAG_SHOW_WHEN_LOCKED, which are
+ * capable of displaying over the lockscreen while the device is still locked (such as Google Maps
+ * navigation).
+ *
+ * Window Manager considers the device to be in the "occluded" state whenever such an activity is on
+ * top of the task stack, including while we're unlocked, while keyguard code considers us to be
+ * occluded only when we're locked, with an occluding activity currently displaying over the
+ * lockscreen.
+ *
+ * This dual definition is confusing, so this repository collects all of the signals WM gives us,
+ * and consolidates them into [showWhenLockedActivityInfo.isOnTop], which is the actual question WM
+ * is answering when they say whether we're 'occluded'. Keyguard then uses this signal to
+ * conditionally transition to [KeyguardState.OCCLUDED] where appropriate.
+ */
+@SysUISingleton
+class KeyguardOcclusionRepository @Inject constructor() {
+    val showWhenLockedActivityInfo = MutableStateFlow(ShowWhenLockedActivityInfo(isOnTop = false))
+
+    /**
+     * Sets whether there's a SHOW_WHEN_LOCKED activity on top of the task stack, and optionally,
+     * information about the activity itself.
+     *
+     * If no value is provided for [taskInfo], we'll default to the current [taskInfo].
+     *
+     * The [taskInfo] is always present when this method is called from the occlude/unocclude
+     * animation runners. We use the default when calling from [KeyguardService.isOccluded], since
+     * we only receive a true/false value there. isOccluded is mostly redundant - it's almost always
+     * called with true after an occlusion animation has started, and with false after an unocclude
+     * animation has started. In those cases, we don't want to clear out the taskInfo just because
+     * it wasn't available at that call site.
+     */
+    fun setShowWhenLockedActivityInfo(
+        onTop: Boolean,
+        taskInfo: RunningTaskInfo? = showWhenLockedActivityInfo.value.taskInfo
+    ) {
+        showWhenLockedActivityInfo.value =
+            ShowWhenLockedActivityInfo(
+                isOnTop = onTop,
+                taskInfo = taskInfo,
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 64e2870..3a6423d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -95,6 +95,7 @@
     val isKeyguardShowing: Flow<Boolean>
 
     /** Is an activity showing over the keyguard? */
+    @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED")
     val isKeyguardOccluded: Flow<Boolean>
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
index 9b3f13d..d9479de 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
@@ -75,7 +75,6 @@
     powerInteractor: PowerInteractor,
     private val scrimLogger: ScrimLogger,
 ) : LightRevealScrimRepository {
-
     companion object {
         val TAG = LightRevealScrimRepository::class.simpleName!!
     }
@@ -156,7 +155,11 @@
     override fun startRevealAmountAnimator(reveal: Boolean) {
         if (reveal == willBeOrIsRevealed) return
         willBeOrIsRevealed = reveal
-        if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse()
+        if (reveal && !revealAmountAnimator.isRunning) {
+            revealAmountAnimator.start()
+        } else {
+            revealAmountAnimator.reverse()
+        }
         scrimLogger.d(TAG, "startRevealAmountAnimator, reveal: ", reveal)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index 6aed944..88ddfd4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -46,13 +47,16 @@
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val communalInteractor: CommunalInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.ALTERNATE_BOUNCER,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
@@ -112,6 +116,11 @@
     }
 
     private fun listenForAlternateBouncerToGone() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            // Handled via #dismissAlternateBouncer.
+            return
+        }
+
         scope.launch {
             keyguardInteractor.isKeyguardGoingAway
                 .sampleUtil(finishedKeyguardState, ::Pair)
@@ -149,6 +158,10 @@
         }
     }
 
+    fun dismissAlternateBouncer() {
+        scope.launch { startTransitionTo(KeyguardState.GONE) }
+    }
+
     companion object {
         const val TAG = "FromAlternateBouncerTransitionInteractor"
         val TRANSITION_DURATION_MS = 300.milliseconds
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index dbd5e26..9040e03 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -27,14 +27,17 @@
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample
 import com.android.systemui.util.kotlin.sample
+import java.util.UUID
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -47,29 +50,118 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.AOD,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
-        listenForAodToLockscreen()
+        listenForAodToAwake()
+        listenForAodToOccluded()
         listenForAodToPrimaryBouncer()
         listenForAodToGone()
-        listenForAodToOccluded()
         listenForTransitionToCamera(scope, keyguardInteractor)
     }
 
     /**
+     * Listen for the signal that we're waking up and figure what state we need to transition to.
+     */
+    private fun listenForAodToAwake() {
+        val transitionToLockscreen: suspend (TransitionStep) -> UUID? =
+            { startedStep: TransitionStep ->
+                val modeOnCanceled =
+                    if (startedStep.from == KeyguardState.LOCKSCREEN) {
+                        TransitionModeOnCanceled.REVERSE
+                    } else if (startedStep.from == KeyguardState.GONE) {
+                        TransitionModeOnCanceled.RESET
+                    } else {
+                        TransitionModeOnCanceled.LAST_VALUE
+                    }
+                startTransitionTo(
+                    toState = KeyguardState.LOCKSCREEN,
+                    modeOnCanceled = modeOnCanceled,
+                )
+            }
+
+        if (KeyguardWmStateRefactor.isEnabled) {
+            // The refactor uses PowerInteractor's wakefulness, which is the earliest wake signal
+            // available. We have all of the information we need at this time to make a decision
+            // about where to transition.
+            scope.launch {
+                powerInteractor.detailedWakefulness
+                    // React only to wake events.
+                    .filter { it.isAwake() }
+                    .sample(
+                        startedKeyguardTransitionStep,
+                        keyguardInteractor.biometricUnlockState,
+                        keyguardInteractor.primaryBouncerShowing,
+                    )
+                    // Make sure we've at least STARTED a transition to AOD.
+                    .filter { (_, startedStep, _, _) -> startedStep.to == KeyguardState.AOD }
+                    .collect { (_, startedStep, biometricUnlockState, primaryBouncerShowing) ->
+                        // Check with the superclass to see if an occlusion transition is needed.
+                        // Also, don't react to wake and unlock events, as we'll be receiving a call
+                        // to #dismissAod() shortly when the authentication completes.
+                        if (
+                            !maybeStartTransitionToOccludedOrInsecureCamera() &&
+                                !isWakeAndUnlock(biometricUnlockState) &&
+                                !primaryBouncerShowing
+                        ) {
+                            transitionToLockscreen(startedStep)
+                        }
+                    }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor
+                    .dozeTransitionTo(DozeStateModel.FINISH)
+                    .sample(
+                        keyguardInteractor.isKeyguardShowing,
+                        startedKeyguardTransitionStep,
+                        keyguardInteractor.isKeyguardOccluded,
+                        keyguardInteractor.biometricUnlockState,
+                        keyguardInteractor.primaryBouncerShowing,
+                    )
+                    .collect {
+                        (
+                            _,
+                            isKeyguardShowing,
+                            lastStartedStep,
+                            occluded,
+                            biometricUnlockState,
+                            primaryBouncerShowing) ->
+                        if (
+                            lastStartedStep.to == KeyguardState.AOD &&
+                                !occluded &&
+                                !isWakeAndUnlock(biometricUnlockState) &&
+                                isKeyguardShowing &&
+                                !primaryBouncerShowing
+                        ) {
+                            transitionToLockscreen(lastStartedStep)
+                        }
+                    }
+            }
+        }
+    }
+
+    /**
      * There are cases where the transition to AOD begins but never completes, such as tapping power
      * during an incoming phone call when unlocked. In this case, GONE->AOD should be interrupted to
      * run AOD->OCCLUDED.
      */
     private fun listenForAodToOccluded() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            // Handled by calls to maybeStartTransitionToOccludedOrInsecureCamera on waking.
+            return
+        }
+
         scope.launch {
             keyguardInteractor.isKeyguardOccluded
                 .sample(startedKeyguardTransitionStep, ::Pair)
@@ -84,49 +176,6 @@
         }
     }
 
-    private fun listenForAodToLockscreen() {
-        scope.launch {
-            keyguardInteractor
-                .dozeTransitionTo(DozeStateModel.FINISH)
-                .sample(
-                    keyguardInteractor.isKeyguardShowing,
-                    startedKeyguardTransitionStep,
-                    keyguardInteractor.isKeyguardOccluded,
-                    keyguardInteractor.biometricUnlockState,
-                    keyguardInteractor.primaryBouncerShowing,
-                )
-                .collect {
-                    (
-                        _,
-                        isKeyguardShowing,
-                        lastStartedStep,
-                        occluded,
-                        biometricUnlockState,
-                        primaryBouncerShowing) ->
-                    if (
-                        lastStartedStep.to == KeyguardState.AOD &&
-                            !occluded &&
-                            !isWakeAndUnlock(biometricUnlockState) &&
-                            isKeyguardShowing &&
-                            !primaryBouncerShowing
-                    ) {
-                        val modeOnCanceled =
-                            if (lastStartedStep.from == KeyguardState.LOCKSCREEN) {
-                                TransitionModeOnCanceled.REVERSE
-                            } else if (lastStartedStep.from == KeyguardState.GONE) {
-                                TransitionModeOnCanceled.RESET
-                            } else {
-                                TransitionModeOnCanceled.LAST_VALUE
-                            }
-                        startTransitionTo(
-                            toState = KeyguardState.LOCKSCREEN,
-                            modeOnCanceled = modeOnCanceled,
-                        )
-                    }
-                }
-        }
-    }
-
     /**
      * If there is a biometric lockout and FPS is tapped while on AOD, it should go directly to the
      * PRIMARY_BOUNCER.
@@ -145,6 +194,7 @@
 
     private fun listenForAodToGone() {
         if (KeyguardWmStateRefactor.isEnabled) {
+            // Handled via #dismissAod.
             return
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 8591fe7..57b2a63 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -34,6 +35,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -46,18 +48,22 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DOZING,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForDozingToAny()
+        listenForWakeFromDozing()
         listenForTransitionToCamera(scope, keyguardInteractor)
     }
 
@@ -70,6 +76,10 @@
         }
 
     private fun listenForDozingToAny() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            return
+        }
+
         scope.launch {
             powerInteractor.isAwake
                 .debounce(50L)
@@ -112,6 +122,58 @@
         }
     }
 
+    /** Figure out what state to transition to when we awake from DOZING. */
+    private fun listenForWakeFromDozing() {
+        if (!KeyguardWmStateRefactor.isEnabled) {
+            return
+        }
+
+        scope.launch {
+            powerInteractor.detailedWakefulness
+                .filter { it.isAwake() }
+                .sample(
+                    startedKeyguardTransitionStep,
+                    communalInteractor.isIdleOnCommunal,
+                    keyguardInteractor.biometricUnlockState,
+                    canDismissLockScreen,
+                    keyguardInteractor.primaryBouncerShowing,
+                )
+                // If we haven't at least STARTED a transition to DOZING, ignore.
+                .filter { (_, startedStep, _, _) -> startedStep.to == KeyguardState.DOZING }
+                .collect {
+                    (
+                        _,
+                        _,
+                        isIdleOnCommunal,
+                        biometricUnlockState,
+                        canDismissLockscreen,
+                        primaryBouncerShowing) ->
+                    if (
+                        !maybeStartTransitionToOccludedOrInsecureCamera() &&
+                            // Handled by dismissFromDozing().
+                            !isWakeAndUnlock(biometricUnlockState)
+                    ) {
+                        startTransitionTo(
+                            if (canDismissLockscreen) {
+                                KeyguardState.GONE
+                            } else if (primaryBouncerShowing) {
+                                KeyguardState.PRIMARY_BOUNCER
+                            } else if (isIdleOnCommunal) {
+                                KeyguardState.GLANCEABLE_HUB
+                            } else {
+                                KeyguardState.LOCKSCREEN
+                            }
+                        )
+                    }
+                }
+        }
+    }
+
+    /** Dismisses keyguard from the DOZING state. */
+    fun dismissFromDozing() {
+        scope.launch { startTransitionTo(KeyguardState.GONE) }
+    }
+
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
             interpolator = Interpolators.LINEAR
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
index a6cdaa8..6433d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
@@ -46,12 +47,16 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 28282c6..3137138 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -23,10 +23,13 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.Utils
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
@@ -35,6 +38,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -48,18 +52,23 @@
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
     private val glanceableHubTransitions: GlanceableHubTransitions,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DREAMING,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForDreamingToOccluded()
         listenForDreamingToGoneWhenDismissable()
         listenForDreamingToGoneFromBiometricUnlock()
+        listenForDreamingToLockscreen()
         listenForDreamingToAodOrDozing()
         listenForTransitionToCamera(scope, keyguardInteractor)
         listenForDreamingToGlanceableHub()
@@ -78,6 +87,7 @@
 
     fun startToLockscreenTransition() {
         scope.launch {
+            KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode()
             if (
                 transitionInteractor.startedKeyguardState.replayCache.last() ==
                     KeyguardState.DREAMING
@@ -88,16 +98,52 @@
     }
 
     private fun listenForDreamingToOccluded() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                combine(
+                        keyguardInteractor.isDreaming,
+                        keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop,
+                        ::Pair
+                    )
+                    .sample(startedKeyguardTransitionStep, ::toTriple)
+                    .filter { (isDreaming, _, startedStep) ->
+                        !isDreaming && startedStep.to == KeyguardState.DREAMING
+                    }
+                    .collect { maybeStartTransitionToOccludedOrInsecureCamera() }
+            }
+        } else {
+            scope.launch {
+                combine(
+                        keyguardInteractor.isKeyguardOccluded,
+                        keyguardInteractor.isDreaming,
+                        ::Pair
+                    )
+                    .sample(startedKeyguardTransitionStep, Utils.Companion::toTriple)
+                    .collect { (isOccluded, isDreaming, lastStartedTransition) ->
+                        if (
+                            isOccluded &&
+                                !isDreaming &&
+                                lastStartedTransition.to == KeyguardState.DREAMING
+                        ) {
+                            startTransitionTo(KeyguardState.OCCLUDED)
+                        }
+                    }
+            }
+        }
+    }
+
+    private fun listenForDreamingToLockscreen() {
+        if (!KeyguardWmStateRefactor.isEnabled) {
+            return
+        }
+
         scope.launch {
-            combine(keyguardInteractor.isKeyguardOccluded, keyguardInteractor.isDreaming, ::Pair)
-                .sample(startedKeyguardTransitionStep, ::toTriple)
-                .collect { (isOccluded, isDreaming, lastStartedTransition) ->
-                    if (
-                        isOccluded &&
-                            !isDreaming &&
-                            lastStartedTransition.to == KeyguardState.DREAMING
-                    ) {
-                        startTransitionTo(KeyguardState.OCCLUDED)
+            keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
+                .filter { onTop -> !onTop }
+                .sample(startedKeyguardState)
+                .collect { startedState ->
+                    if (startedState == KeyguardState.DREAMING) {
+                        startTransitionTo(KeyguardState.LOCKSCREEN)
                     }
                 }
         }
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 786c3c6..51bc3ae 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
@@ -23,6 +23,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
@@ -35,6 +36,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -49,14 +51,18 @@
     private val keyguardInteractor: KeyguardInteractor,
     override val transitionRepository: KeyguardTransitionRepository,
     transitionInteractor: KeyguardTransitionInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.GLANCEABLE_HUB,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
+
     override fun start() {
         if (!Flags.communalHub()) {
             return
@@ -151,14 +157,27 @@
     }
 
     private fun listenForHubToOccluded() {
-        scope.launch {
-            and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming))
-                .sample(startedKeyguardState, ::Pair)
-                .collect { (isOccludedAndNotDreaming, keyguardState) ->
-                    if (isOccludedAndNotDreaming && keyguardState == fromState) {
-                        startTransitionTo(KeyguardState.OCCLUDED)
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
+                    .filter { onTop -> onTop }
+                    .sample(startedKeyguardState)
+                    .collect {
+                        if (it == KeyguardState.GLANCEABLE_HUB) {
+                            maybeStartTransitionToOccludedOrInsecureCamera()
+                        }
                     }
-                }
+            }
+        } else {
+            scope.launch {
+                and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming))
+                    .sample(startedKeyguardState, ::Pair)
+                    .collect { (isOccludedAndNotDreaming, keyguardState) ->
+                        if (isOccludedAndNotDreaming && keyguardState == fromState) {
+                            startTransitionTo(KeyguardState.OCCLUDED)
+                        }
+                    }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 7593ac2..d5a9bd1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -22,6 +22,8 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
@@ -34,6 +36,8 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -46,14 +50,18 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    private val biometricSettingsRepository: BiometricSettingsRepository,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.GONE,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
@@ -65,23 +73,46 @@
 
     // Primarily for when the user chooses to lock down the device
     private fun listenForGoneToLockscreenOrHub() {
-        scope.launch {
-            keyguardInteractor.isKeyguardShowing
-                .sample(
-                    startedKeyguardState,
-                    communalInteractor.isIdleOnCommunal,
-                )
-                .collect { (isKeyguardShowing, startedState, isIdleOnCommunal) ->
-                    if (isKeyguardShowing && startedState == KeyguardState.GONE) {
-                        val to =
-                            if (isIdleOnCommunal) {
-                                KeyguardState.GLANCEABLE_HUB
-                            } else {
-                                KeyguardState.LOCKSCREEN
-                            }
-                        startTransitionTo(to)
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                biometricSettingsRepository.isCurrentUserInLockdown
+                    .distinctUntilChanged()
+                    .filter { inLockdown -> inLockdown }
+                    .sample(
+                        startedKeyguardState,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect { (_, startedState, isIdleOnCommunal) ->
+                        if (startedState == KeyguardState.GONE) {
+                            val to =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(to, ownerReason = "User initiated lockdown")
+                        }
                     }
-                }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor.isKeyguardShowing
+                    .sample(
+                        startedKeyguardState,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect { (isKeyguardShowing, startedState, isIdleOnCommunal) ->
+                        if (isKeyguardShowing && startedState == KeyguardState.GONE) {
+                            val to =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(to)
+                        }
+                    }
+            }
         }
     }
 
@@ -122,24 +153,10 @@
 
     private fun listenForGoneToAodOrDozing() {
         scope.launch {
-            powerInteractor.isAsleep
-                .sample(
-                    combine(
-                        startedKeyguardTransitionStep,
-                        keyguardInteractor.isAodAvailable,
-                        ::Pair
-                    ),
-                    ::toTriple
-                )
-                .collect { (isAsleep, lastStartedStep, isAodAvailable) ->
-                    if (lastStartedStep.to == KeyguardState.GONE && isAsleep) {
-                        startTransitionTo(
-                            toState =
-                                if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING,
-                            modeOnCanceled = TransitionModeOnCanceled.RESET,
-                        )
-                    }
-                }
+            listenForSleepTransition(
+                from = KeyguardState.GONE,
+                modeOnCanceledFromStartedStep = { TransitionModeOnCanceled.RESET },
+            )
         }
     }
 
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 cb1571e..bcad332 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
@@ -33,7 +33,6 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.util.kotlin.Utils.Companion.toQuad
-import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
 import java.util.UUID
 import javax.inject.Inject
@@ -44,6 +43,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
@@ -62,21 +62,24 @@
     private val keyguardInteractor: KeyguardInteractor,
     private val flags: FeatureFlags,
     private val shadeRepository: ShadeRepository,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val glanceableHubTransitions: GlanceableHubTransitions,
     private val swipeToDismissInteractor: SwipeToDismissInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.LOCKSCREEN,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForLockscreenToGone()
         listenForLockscreenToGoneDragging()
-        listenForLockscreenToOccluded()
+        listenForLockscreenToOccludedOrDreaming()
         listenForLockscreenToAodOrDozing()
         listenForLockscreenToPrimaryBouncer()
         listenForLockscreenToDreaming()
@@ -115,6 +118,10 @@
     }
 
     private fun listenForLockscreenToDreaming() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            return
+        }
+
         val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING)
         scope.launch {
             keyguardInteractor.isAbleToDream
@@ -157,7 +164,10 @@
                     if (
                         isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
                     ) {
-                        startTransitionTo(KeyguardState.PRIMARY_BOUNCER)
+                        startTransitionTo(
+                            KeyguardState.PRIMARY_BOUNCER,
+                            ownerReason = "#listenForLockscreenToPrimaryBouncer"
+                        )
                     }
                 }
         }
@@ -254,7 +264,8 @@
                                 transitionId =
                                     startTransitionTo(
                                         toState = KeyguardState.PRIMARY_BOUNCER,
-                                        animator = null, // transition will be manually controlled
+                                        animator = null, // transition will be manually controlled,
+                                        ownerReason = "#listenForLockscreenToPrimaryBouncerDragging"
                                     )
                             }
                         }
@@ -309,47 +320,51 @@
         }
     }
 
-    private fun listenForLockscreenToOccluded() {
-        scope.launch {
-            keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect {
-                (isOccluded, keyguardState) ->
-                if (isOccluded && keyguardState == KeyguardState.LOCKSCREEN) {
-                    startTransitionTo(KeyguardState.OCCLUDED)
-                }
+    private fun listenForLockscreenToOccludedOrDreaming() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardOcclusionInteractor.showWhenLockedActivityInfo
+                    .filter { it.isOnTop }
+                    .sample(startedKeyguardState, ::Pair)
+                    .collect { (taskInfo, startedState) ->
+                        if (startedState == KeyguardState.LOCKSCREEN) {
+                            startTransitionTo(
+                                if (taskInfo.isDream()) {
+                                    KeyguardState.DREAMING
+                                } else {
+                                    KeyguardState.OCCLUDED
+                                }
+                            )
+                        }
+                    }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor.isKeyguardOccluded
+                    .sample(startedKeyguardState, ::Pair)
+                    .collect { (isOccluded, keyguardState) ->
+                        if (isOccluded && keyguardState == KeyguardState.LOCKSCREEN) {
+                            startTransitionTo(KeyguardState.OCCLUDED)
+                        }
+                    }
             }
         }
     }
 
     private fun listenForLockscreenToAodOrDozing() {
         scope.launch {
-            powerInteractor.isAsleep
-                .sample(
-                    combine(
-                        startedKeyguardTransitionStep,
-                        keyguardInteractor.isAodAvailable,
-                        ::Pair
-                    ),
-                    ::toTriple
-                )
-                .collect { (isAsleep, lastStartedStep, isAodAvailable) ->
-                    if (lastStartedStep.to == KeyguardState.LOCKSCREEN && isAsleep) {
-                        val toState =
-                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
-                        val modeOnCanceled =
-                            if (
-                                toState == KeyguardState.AOD &&
-                                    lastStartedStep.from == KeyguardState.AOD
-                            ) {
-                                TransitionModeOnCanceled.REVERSE
-                            } else {
-                                TransitionModeOnCanceled.LAST_VALUE
-                            }
-                        startTransitionTo(
-                            toState = toState,
-                            modeOnCanceled = modeOnCanceled,
-                        )
+            listenForSleepTransition(
+                from = KeyguardState.LOCKSCREEN,
+                modeOnCanceledFromStartedStep = { startedStep ->
+                    if (
+                        startedStep.to == KeyguardState.AOD && startedStep.from == KeyguardState.AOD
+                    ) {
+                        TransitionModeOnCanceled.REVERSE
+                    } else {
+                        TransitionModeOnCanceled.LAST_VALUE
                     }
                 }
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index efb604d..f10327e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -22,17 +22,17 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample
-import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -45,20 +45,23 @@
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.OCCLUDED,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForOccludedToLockscreenOrHub()
         listenForOccludedToDreaming()
-        listenForOccludedToAodOrDozing()
+        listenForOccludedToAsleep()
         listenForOccludedToGone()
         listenForOccludedToAlternateBouncer()
         listenForOccludedToPrimaryBouncer()
@@ -90,43 +93,72 @@
     }
 
     private fun listenForOccludedToLockscreenOrHub() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
+                    .filter { onTop -> !onTop }
+                    .sample(
+                        startedKeyguardState,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect { (_, startedState, isIdleOnCommunal) ->
+                        // Occlusion signals come from the framework, and should interrupt any
+                        // existing transition
+                        if (startedState == KeyguardState.OCCLUDED) {
+                            val to =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(to)
+                        }
+                    }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor.isKeyguardOccluded
+                    .sample(
+                        keyguardInteractor.isKeyguardShowing,
+                        startedKeyguardTransitionStep,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect { (isOccluded, isShowing, lastStartedKeyguardState, isIdleOnCommunal)
+                        ->
+                        // Occlusion signals come from the framework, and should interrupt any
+                        // existing transition
+                        if (
+                            !isOccluded &&
+                                isShowing &&
+                                lastStartedKeyguardState.to == KeyguardState.OCCLUDED
+                        ) {
+                            val to =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(to)
+                        }
+                    }
+            }
+        }
+    }
+
+    private fun listenForOccludedToGone() {
+        if (KeyguardWmStateRefactor.isEnabled) {
+            // We don't think OCCLUDED to GONE is possible. You should always have to go via a
+            // *_BOUNCER state to end up GONE. Launching an activity over a dismissable keyguard
+            // dismisses it, and even "extend unlock" doesn't unlock the device in the background.
+            // If we're wrong - sorry, add it back here.
+            return
+        }
+
         scope.launch {
             keyguardInteractor.isKeyguardOccluded
                 .sample(
                     keyguardInteractor.isKeyguardShowing,
                     startedKeyguardTransitionStep,
-                    communalInteractor.isIdleOnCommunal,
-                )
-                .collect { (isOccluded, isShowing, lastStartedKeyguardState, isIdleOnCommunal) ->
-                    // Occlusion signals come from the framework, and should interrupt any
-                    // existing transition
-                    if (
-                        !isOccluded &&
-                            isShowing &&
-                            lastStartedKeyguardState.to == KeyguardState.OCCLUDED
-                    ) {
-                        val to =
-                            if (isIdleOnCommunal) {
-                                KeyguardState.GLANCEABLE_HUB
-                            } else {
-                                KeyguardState.LOCKSCREEN
-                            }
-                        startTransitionTo(to)
-                    }
-                }
-        }
-    }
-
-    private fun listenForOccludedToGone() {
-        scope.launch {
-            keyguardInteractor.isKeyguardOccluded
-                .sample(
-                    combine(
-                        keyguardInteractor.isKeyguardShowing,
-                        startedKeyguardTransitionStep,
-                        ::Pair
-                    ),
-                    ::toTriple
                 )
                 .collect { (isOccluded, isShowing, lastStartedKeyguardState) ->
                     // Occlusion signals come from the framework, and should interrupt any
@@ -142,25 +174,12 @@
         }
     }
 
-    private fun listenForOccludedToAodOrDozing() {
-        scope.launch {
-            powerInteractor.isAsleep
-                .sample(
-                    combine(
-                        startedKeyguardTransitionStep,
-                        keyguardInteractor.isAodAvailable,
-                        ::Pair
-                    ),
-                    ::toTriple
-                )
-                .collect { (isAsleep, lastStartedStep, isAodAvailable) ->
-                    if (lastStartedStep.to == KeyguardState.OCCLUDED && isAsleep) {
-                        startTransitionTo(
-                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
-                        )
-                    }
-                }
-        }
+    fun dismissToGone() {
+        scope.launch { startTransitionTo(KeyguardState.GONE) }
+    }
+
+    private fun listenForOccludedToAsleep() {
+        scope.launch { listenForSleepTransition(from = KeyguardState.OCCLUDED) }
     }
 
     private fun listenForOccludedToAlternateBouncer() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index c5a2846..391dccc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample
-import com.android.systemui.util.kotlin.Utils.Companion.toQuad
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
 import com.android.wm.shell.animation.Interpolators
@@ -42,6 +41,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
 
@@ -59,18 +59,21 @@
     private val flags: FeatureFlags,
     private val keyguardSecurityModel: KeyguardSecurityModel,
     private val selectedUserInteractor: SelectedUserInteractor,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.PRIMARY_BOUNCER,
         transitionInteractor = transitionInteractor,
         mainDispatcher = mainDispatcher,
         bgDispatcher = bgDispatcher,
+        powerInteractor = powerInteractor,
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
     ) {
 
     override fun start() {
         listenForPrimaryBouncerToGone()
-        listenForPrimaryBouncerToAodOrDozing()
+        listenForPrimaryBouncerToAsleep()
         listenForPrimaryBouncerToLockscreenHubOrOccluded()
         listenForPrimaryBouncerToDreamingLockscreenHosted()
         listenForTransitionToCamera(scope, keyguardInteractor)
@@ -128,72 +131,86 @@
     }
 
     private fun listenForPrimaryBouncerToLockscreenHubOrOccluded() {
-        scope.launch {
-            keyguardInteractor.primaryBouncerShowing
-                .sample(
-                    powerInteractor.isAwake,
-                    startedKeyguardTransitionStep,
-                    keyguardInteractor.isKeyguardOccluded,
-                    keyguardInteractor.isDreaming,
-                    keyguardInteractor.isActiveDreamLockscreenHosted,
-                    communalInteractor.isIdleOnCommunal,
-                )
-                .collect {
-                    (
-                        isBouncerShowing,
-                        isAwake,
-                        lastStartedTransitionStep,
-                        occluded,
-                        isDreaming,
-                        isActiveDreamLockscreenHosted,
-                        isIdleOnCommunal) ->
-                    if (
-                        !isBouncerShowing &&
-                            lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER &&
-                            isAwake &&
-                            !isActiveDreamLockscreenHosted
-                    ) {
-                        val toState =
-                            if (occluded && !isDreaming) {
-                                KeyguardState.OCCLUDED
-                            } else if (isIdleOnCommunal) {
-                                KeyguardState.GLANCEABLE_HUB
-                            } else if (isDreaming) {
-                                KeyguardState.DREAMING
-                            } else {
-                                KeyguardState.LOCKSCREEN
-                            }
-                        startTransitionTo(toState)
+        if (KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardInteractor.primaryBouncerShowing
+                    .sample(
+                        startedKeyguardTransitionStep,
+                        powerInteractor.isAwake,
+                        keyguardInteractor.isActiveDreamLockscreenHosted,
+                        communalInteractor.isIdleOnCommunal
+                    )
+                    .filter { (_, startedStep, _, _) ->
+                        startedStep.to == KeyguardState.PRIMARY_BOUNCER
                     }
-                }
+                    .collect {
+                        (
+                            isBouncerShowing,
+                            _,
+                            isAwake,
+                            isActiveDreamLockscreenHosted,
+                            isIdleOnCommunal) ->
+                        if (
+                            !maybeStartTransitionToOccludedOrInsecureCamera() &&
+                                !isBouncerShowing &&
+                                isAwake &&
+                                !isActiveDreamLockscreenHosted
+                        ) {
+                            val toState =
+                                if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(toState)
+                        }
+                    }
+            }
+        } else {
+            scope.launch {
+                keyguardInteractor.primaryBouncerShowing
+                    .sample(
+                        powerInteractor.isAwake,
+                        startedKeyguardTransitionStep,
+                        keyguardInteractor.isKeyguardOccluded,
+                        keyguardInteractor.isDreaming,
+                        keyguardInteractor.isActiveDreamLockscreenHosted,
+                        communalInteractor.isIdleOnCommunal,
+                    )
+                    .collect {
+                        (
+                            isBouncerShowing,
+                            isAwake,
+                            lastStartedTransitionStep,
+                            occluded,
+                            isDreaming,
+                            isActiveDreamLockscreenHosted,
+                            isIdleOnCommunal) ->
+                        if (
+                            !isBouncerShowing &&
+                                lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER &&
+                                isAwake &&
+                                !isActiveDreamLockscreenHosted
+                        ) {
+                            val toState =
+                                if (occluded && !isDreaming) {
+                                    KeyguardState.OCCLUDED
+                                } else if (isIdleOnCommunal) {
+                                    KeyguardState.GLANCEABLE_HUB
+                                } else if (isDreaming) {
+                                    KeyguardState.DREAMING
+                                } else {
+                                    KeyguardState.LOCKSCREEN
+                                }
+                            startTransitionTo(toState)
+                        }
+                    }
+            }
         }
     }
 
-    private fun listenForPrimaryBouncerToAodOrDozing() {
-        scope.launch {
-            keyguardInteractor.primaryBouncerShowing
-                .sample(
-                    combine(
-                        powerInteractor.isAsleep,
-                        startedKeyguardTransitionStep,
-                        keyguardInteractor.isAodAvailable,
-                        ::Triple
-                    ),
-                    ::toQuad
-                )
-                .collect { (isBouncerShowing, isAsleep, lastStartedTransitionStep, isAodAvailable)
-                    ->
-                    if (
-                        !isBouncerShowing &&
-                            lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER &&
-                            isAsleep
-                    ) {
-                        startTransitionTo(
-                            if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING
-                        )
-                    }
-                }
-        }
+    private fun listenForPrimaryBouncerToAsleep() {
+        scope.launch { listenForSleepTransition(from = KeyguardState.PRIMARY_BOUNCER) }
     }
 
     private fun listenForPrimaryBouncerToDreamingLockscreenHosted() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 5410b10..f321bd7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -163,15 +163,18 @@
             .distinctUntilChanged()
 
     /** Whether the keyguard is showing or not. */
+    @Deprecated("Use KeyguardTransitionInteractor + KeyguardState")
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
 
     /** Whether the keyguard is dismissible or not. */
     val isKeyguardDismissible: Flow<Boolean> = repository.isKeyguardDismissible
 
     /** Whether the keyguard is occluded (covered by an activity). */
+    @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED")
     val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded
 
     /** Whether the keyguard is going away. */
+    @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.GONE")
     val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
 
     /** Keyguard can be clipped at the top as the shade is dragged */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt
new file mode 100644
index 0000000..9aa2202
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.app.ActivityManager.RunningTaskInfo
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardOcclusionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Logic related to keyguard occlusion. The keyguard is occluded when an activity with
+ * FLAG_SHOW_WHEN_LOCKED is on top of the activity task stack, with that activity displaying on top
+ * of ("occluding") the lockscreen UI. Common examples of this are Google Maps Navigation and the
+ * secure camera.
+ *
+ * This should usually be used only by keyguard internal classes. Most System UI use cases should
+ * use [KeyguardTransitionInteractor] to see if we're in [KeyguardState.OCCLUDED] instead.
+ */
+@SysUISingleton
+class KeyguardOcclusionInteractor
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    val repository: KeyguardOcclusionRepository,
+    val powerInteractor: PowerInteractor,
+    val transitionInteractor: KeyguardTransitionInteractor,
+    val keyguardInteractor: KeyguardInteractor,
+) {
+    val showWhenLockedActivityInfo = repository.showWhenLockedActivityInfo.asStateFlow()
+
+    /**
+     * Whether a SHOW_WHEN_LOCKED activity is on top of the task stack. This does not necessarily
+     * mean we're OCCLUDED, as we could be GONE (unlocked), with an activity that can (but is not
+     * currently) displaying over the lockscreen.
+     *
+     * Transition interactors use this to determine when we should transition to the OCCLUDED state.
+     *
+     * Outside of the transition/occlusion interactors, you almost certainly don't want to use this.
+     * Instead, use KeyguardTransitionInteractor to figure out if we're in KeyguardState.OCCLUDED.
+     */
+    val isShowWhenLockedActivityOnTop = showWhenLockedActivityInfo.map { it.isOnTop }
+
+    /** Whether we should start a transition due to the power button launch gesture. */
+    fun shouldTransitionFromPowerButtonGesture(): Boolean {
+        // powerButtonLaunchGestureTriggered remains true while we're awake from a power button
+        // gesture. Check that we were asleep or transitioning to asleep before starting a
+        // transition, to ensure we don't transition while moving between, for example,
+        // *_BOUNCER -> LOCKSCREEN.
+        return powerInteractor.detailedWakefulness.value.powerButtonLaunchGestureTriggered &&
+            KeyguardState.deviceIsAsleepInState(transitionInteractor.getStartedState())
+    }
+
+    /**
+     * Whether the SHOW_WHEN_LOCKED activity was launched from the double tap power button gesture.
+     * This remains true while the activity is running and emits false once it is killed.
+     */
+    val showWhenLockedActivityLaunchedFromPowerGesture =
+        merge(
+                // Emit true when the power launch gesture is triggered, since this means a
+                // SHOW_WHEN_LOCKED activity will be launched from the gesture (unless we're
+                // currently
+                // GONE, in which case we're going back to GONE and launching the insecure camera).
+                powerInteractor.detailedWakefulness
+                    .sample(transitionInteractor.currentKeyguardState, ::Pair)
+                    .map { (wakefulness, currentKeyguardState) ->
+                        wakefulness.powerButtonLaunchGestureTriggered &&
+                            currentKeyguardState != KeyguardState.GONE
+                    },
+                // Emit false once that activity goes away.
+                isShowWhenLockedActivityOnTop.filter { !it }.map { false }
+            )
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
+    /**
+     * Whether launching an occluding activity will automatically dismiss keyguard. This happens if
+     * the keyguard is dismissable.
+     */
+    val occludingActivityWillDismissKeyguard =
+        keyguardInteractor.isKeyguardDismissible.stateIn(scope, SharingStarted.Eagerly, false)
+
+    /**
+     * Called to let System UI know that WM says a SHOW_WHEN_LOCKED activity is on top (or no longer
+     * on top).
+     *
+     * This signal arrives from WM when a SHOW_WHEN_LOCKED activity is started or killed - it is
+     * never set directly by System UI. While we might be the reason the activity was started
+     * (launching the camera from the power button gesture), we ultimately only receive this signal
+     * once that activity starts. It's up to us to start the appropriate keyguard transitions,
+     * because that activity is going to be visible (or not) regardless.
+     */
+    fun setWmNotifiedShowWhenLockedActivityOnTop(
+        showWhenLockedActivityOnTop: Boolean,
+        taskInfo: RunningTaskInfo? = null
+    ) {
+        repository.setShowWhenLockedActivityInfo(showWhenLockedActivityOnTop, taskInfo)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index d81f1f1..c28e49d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -41,6 +41,7 @@
     private val powerInteractor: PowerInteractor,
     private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     private val shadeInteractor: ShadeInteractor,
+    private val keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) {
 
     fun start() {
@@ -91,6 +92,12 @@
         }
 
         scope.launch {
+            keyguardInteractor.isKeyguardDismissible.collect {
+                logger.log(TAG, VERBOSE, "isKeyguardDismissable", it)
+            }
+        }
+
+        scope.launch {
             keyguardInteractor.isAbleToDream.collect {
                 logger.log(TAG, VERBOSE, "isAbleToDream", it)
             }
@@ -125,5 +132,11 @@
                 logger.log(TAG, VERBOSE, "onCameraLaunchDetected", it)
             }
         }
+
+        scope.launch {
+            keyguardOcclusionInteractor.showWhenLockedActivityInfo.collect {
+                logger.log(TAG, VERBOSE, "showWhenLockedActivityInfo", it)
+            }
+        }
     }
 }
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 37b331c..00902b4 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
@@ -20,6 +20,7 @@
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
@@ -42,6 +43,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
@@ -56,11 +58,15 @@
 @Inject
 constructor(
     @Application val scope: CoroutineScope,
+    private val keyguardRepository: KeyguardRepository,
     private val repository: KeyguardTransitionRepository,
     private val fromLockscreenTransitionInteractor: dagger.Lazy<FromLockscreenTransitionInteractor>,
     private val fromPrimaryBouncerTransitionInteractor:
         dagger.Lazy<FromPrimaryBouncerTransitionInteractor>,
     private val fromAodTransitionInteractor: dagger.Lazy<FromAodTransitionInteractor>,
+    private val fromAlternateBouncerTransitionInteractor:
+        dagger.Lazy<FromAlternateBouncerTransitionInteractor>,
+    private val fromDozingTransitionInteractor: dagger.Lazy<FromDozingTransitionInteractor>,
 ) {
     private val TAG = this::class.simpleName
 
@@ -207,6 +213,12 @@
             .map { step -> step.to }
             .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
+    /** Which keyguard state to use when the device goes to sleep. */
+    val asleepKeyguardState: StateFlow<KeyguardState> =
+        keyguardRepository.isAodAvailable
+            .map { aodAvailable -> if (aodAvailable) AOD else DOZING }
+            .stateIn(scope, SharingStarted.Eagerly, DOZING)
+
     /**
      * A pair of the most recent STARTED step, and the transition step immediately preceding it. The
      * transition framework enforces that the previous step is either a CANCELED or FINISHED step,
@@ -368,7 +380,10 @@
         when (val startedState = startedKeyguardState.replayCache.last()) {
             LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
             PRIMARY_BOUNCER -> fromPrimaryBouncerTransitionInteractor.get().dismissPrimaryBouncer()
+            ALTERNATE_BOUNCER ->
+                fromAlternateBouncerTransitionInteractor.get().dismissAlternateBouncer()
             AOD -> fromAodTransitionInteractor.get().dismissAod()
+            DOZING -> fromDozingTransitionInteractor.get().dismissFromDozing()
             else ->
                 Log.e(
                     "KeyguardTransitionInteractor",
@@ -421,12 +436,17 @@
         fromStatePredicate: (KeyguardState) -> Boolean,
         toStatePredicate: (KeyguardState) -> Boolean,
     ): Flow<Boolean> {
+        return isInTransitionWhere { from, to -> fromStatePredicate(from) && toStatePredicate(to) }
+    }
+
+    fun isInTransitionWhere(
+        fromToStatePredicate: (KeyguardState, KeyguardState) -> Boolean
+    ): Flow<Boolean> {
         return repository.transitions
             .filter { it.transitionState != TransitionState.CANCELED }
             .mapLatest {
                 it.transitionState != TransitionState.FINISHED &&
-                    fromStatePredicate(it.from) &&
-                    toStatePredicate(it.to)
+                    fromToStatePredicate(it.from, it.to)
             }
             .distinctUntilChanged()
     }
@@ -447,4 +467,16 @@
      */
     fun isFinishedInStateWhereValue(stateMatcher: (KeyguardState) -> Boolean) =
         stateMatcher(finishedKeyguardState.replayCache.last())
+
+    fun getCurrentState(): KeyguardState {
+        return currentKeyguardState.replayCache.last()
+    }
+
+    fun getStartedState(): KeyguardState {
+        return startedKeyguardState.replayCache.last()
+    }
+
+    fun getFinishedState(): KeyguardState {
+        return finishedKeyguardState.replayCache.last()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 4d731ec..8905c9e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -43,7 +43,6 @@
     private val scrimLogger: ScrimLogger,
     private val powerInteractor: PowerInteractor,
 ) {
-
     init {
         listenForStartedKeyguardTransitionStep()
     }
@@ -52,9 +51,7 @@
         scope.launch {
             transitionInteractor.startedKeyguardTransitionStep.collect {
                 scrimLogger.d(TAG, "listenForStartedKeyguardTransitionStep", it)
-                lightRevealScrimRepository.startRevealAmountAnimator(
-                    willBeRevealedInState(it.to),
-                )
+                lightRevealScrimRepository.startRevealAmountAnimator(willBeRevealedInState(it.to))
             }
         }
     }
@@ -89,25 +86,25 @@
 
     companion object {
 
-    /**
-     * Whether the light reveal scrim will be fully revealed (revealAmount = 1.0f) in the given
-     * state after the transition is complete. If false, scrim will be fully hidden.
-     */
-    private fun willBeRevealedInState(state: KeyguardState): Boolean {
-        return when (state) {
-            KeyguardState.OFF -> false
-            KeyguardState.DOZING -> false
-            KeyguardState.AOD -> false
-            KeyguardState.DREAMING -> true
-            KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> true
-            KeyguardState.GLANCEABLE_HUB -> true
-            KeyguardState.ALTERNATE_BOUNCER -> true
-            KeyguardState.PRIMARY_BOUNCER -> true
-            KeyguardState.LOCKSCREEN -> true
-            KeyguardState.GONE -> true
-            KeyguardState.OCCLUDED -> true
+        /**
+         * Whether the light reveal scrim will be fully revealed (revealAmount = 1.0f) in the given
+         * state after the transition is complete. If false, scrim will be fully hidden.
+         */
+        private fun willBeRevealedInState(state: KeyguardState): Boolean {
+            return when (state) {
+                KeyguardState.OFF -> false
+                KeyguardState.DOZING -> false
+                KeyguardState.AOD -> false
+                KeyguardState.DREAMING -> true
+                KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> true
+                KeyguardState.GLANCEABLE_HUB -> true
+                KeyguardState.ALTERNATE_BOUNCER -> true
+                KeyguardState.PRIMARY_BOUNCER -> true
+                KeyguardState.LOCKSCREEN -> true
+                KeyguardState.GONE -> true
+                KeyguardState.OCCLUDED -> true
+            }
         }
-    }
 
         val TAG = LightRevealScrimInteractor::class.simpleName!!
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 3ccbdba..375df3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -18,15 +18,20 @@
 
 import android.animation.ValueAnimator
 import android.util.Log
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 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.TransitionModeOnCanceled
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.sample
 import java.util.UUID
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -46,6 +51,8 @@
     val transitionInteractor: KeyguardTransitionInteractor,
     val mainDispatcher: CoroutineDispatcher,
     val bgDispatcher: CoroutineDispatcher,
+    val powerInteractor: PowerInteractor,
+    val keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) {
     val name = this::class.simpleName ?: "UnknownTransitionInteractor"
     abstract val transitionRepository: KeyguardTransitionRepository
@@ -65,7 +72,11 @@
     suspend fun startTransitionTo(
         toState: KeyguardState,
         animator: ValueAnimator? = getDefaultAnimatorForTransitionsToState(toState),
-        modeOnCanceled: TransitionModeOnCanceled = TransitionModeOnCanceled.LAST_VALUE
+        modeOnCanceled: TransitionModeOnCanceled = TransitionModeOnCanceled.LAST_VALUE,
+        // Even more information about why the owner started this transition, if this is a dangerous
+        // transition (*cough* occlusion) where you'd be sad to not have all the info you can get in
+        // a bugreport.
+        ownerReason: String = "",
     ): UUID? {
         if (
             fromState != transitionInteractor.startedKeyguardState.replayCache.last() &&
@@ -85,7 +96,7 @@
         return withContext(mainDispatcher) {
             transitionRepository.startTransition(
                 TransitionInfo(
-                    name,
+                    name + if (ownerReason.isNotBlank()) "($ownerReason)" else "",
                     fromState,
                     toState,
                     animator,
@@ -95,24 +106,107 @@
         }
     }
 
+    /**
+     * Check whether we need to transition to [KeyguardState.OCCLUDED], based on the presence of a
+     * SHOW_WHEN_LOCKED activity, or back to [KeyguardState.GONE], for some power button launch
+     * gesture cases. If so, start the transition.
+     *
+     * Returns true if a transition was started, false otherwise.
+     */
+    suspend fun maybeStartTransitionToOccludedOrInsecureCamera(): Boolean {
+        if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) {
+            if (transitionInteractor.getCurrentState() == KeyguardState.GONE) {
+                // If the current state is GONE when the launch gesture is triggered, it means we
+                // were in transition from GONE -> DOZING/AOD due to the first power button tap. The
+                // second tap indicates that the user's intent was actually to launch the unlocked
+                // (insecure) camera, so we should transition back to GONE.
+                startTransitionTo(
+                    KeyguardState.GONE,
+                    ownerReason = "Power button gesture while GONE"
+                )
+            } else if (keyguardOcclusionInteractor.occludingActivityWillDismissKeyguard.value) {
+                // The double tap gesture occurred while not GONE (AOD/LOCKSCREEN/etc.), but the
+                // keyguard is dismissable. The activity launch will dismiss the keyguard, so we
+                // should transition to GONE.
+                startTransitionTo(
+                    KeyguardState.GONE,
+                    ownerReason = "Power button gesture on dismissable keyguard"
+                )
+            } else {
+                // Otherwise, the double tap gesture occurred while not GONE and not dismissable,
+                // which means we will launch the secure camera, which OCCLUDES the keyguard.
+                startTransitionTo(
+                    KeyguardState.OCCLUDED,
+                    ownerReason = "Power button gesture on lockscreen"
+                )
+            }
+
+            return true
+        } else if (keyguardOcclusionInteractor.showWhenLockedActivityInfo.value.isOnTop) {
+            // A SHOW_WHEN_LOCKED activity is on top of the task stack. Transition to OCCLUDED so
+            // it's visible.
+            // TODO(b/307976454) - Centralize transition to DREAMING here.
+            startTransitionTo(
+                KeyguardState.OCCLUDED,
+                ownerReason = "SHOW_WHEN_LOCKED activity on top"
+            )
+
+            return true
+        } else {
+            // No transition needed, let the interactor figure out where to go.
+            return false
+        }
+    }
+
+    /**
+     * Transition to the appropriate state when the device goes to sleep while in [from].
+     *
+     * We could also just use [fromState], but it's more readable in the From*TransitionInteractor
+     * if you're explicitly declaring which state you're listening from. If you passed in the wrong
+     * state, [startTransitionTo] would complain anyway.
+     */
+    suspend fun listenForSleepTransition(
+        from: KeyguardState,
+        modeOnCanceledFromStartedStep: (TransitionStep) -> TransitionModeOnCanceled = {
+            TransitionModeOnCanceled.LAST_VALUE
+        }
+    ) {
+        powerInteractor.isAsleep
+            .filter { isAsleep -> isAsleep }
+            .sample(startedKeyguardTransitionStep)
+            .filter { startedStep -> startedStep.to == from }
+            .map(modeOnCanceledFromStartedStep)
+            .collect { modeOnCanceled ->
+                startTransitionTo(
+                    toState = transitionInteractor.asleepKeyguardState.value,
+                    modeOnCanceled = modeOnCanceled,
+                    ownerReason = "Sleep transition triggered"
+                )
+            }
+    }
+
     /** This signal may come in before the occlusion signal, and can provide a custom transition */
     fun listenForTransitionToCamera(
         scope: CoroutineScope,
         keyguardInteractor: KeyguardInteractor,
     ) {
-        scope.launch {
-            keyguardInteractor.onCameraLaunchDetected
-                .sample(transitionInteractor.finishedKeyguardState)
-                .collect { finishedKeyguardState ->
-                    // Other keyguard state transitions may trigger on the first power button push,
-                    // so use the last finishedKeyguardState to determine the overriding FROM state
-                    if (finishedKeyguardState == fromState) {
-                        startTransitionTo(
-                            toState = KeyguardState.OCCLUDED,
-                            modeOnCanceled = TransitionModeOnCanceled.RESET,
-                        )
+        if (!KeyguardWmStateRefactor.isEnabled) {
+            scope.launch {
+                keyguardInteractor.onCameraLaunchDetected
+                    .sample(transitionInteractor.finishedKeyguardState)
+                    .collect { finishedKeyguardState ->
+                        // Other keyguard state transitions may trigger on the first power button
+                        // push,
+                        // so use the last finishedKeyguardState to determine the overriding FROM
+                        // state
+                        if (finishedKeyguardState == fromState) {
+                            startTransitionTo(
+                                toState = KeyguardState.OCCLUDED,
+                                modeOnCanceled = TransitionModeOnCanceled.RESET,
+                            )
+                        }
                     }
-                }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
index a9eec18..7e39a88 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt
@@ -43,6 +43,10 @@
             to = KeyguardState.GONE,
         )
 
+    /**
+     * AOD -> GONE should fade out the lockscreen contents. This transition plays both during wake
+     * and unlock, and also during insecure camera launch (which is GONE -> AOD (canceled) -> GONE).
+     */
     fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
         var startAlpha = 1f
         return transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
index 105a7ed..445575f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
@@ -16,12 +16,15 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
 
 /** Breaks down AOD->OCCLUDED transition into discrete steps for corresponding views to consume. */
 @SysUISingleton
@@ -37,5 +40,23 @@
             to = KeyguardState.OCCLUDED,
         )
 
+    /**
+     * Fade out the lockscreen during a transition to OCCLUDED.
+     *
+     * This happens when pressing the power button while a SHOW_WHEN_LOCKED activity is on the top
+     * of the task stack, as well as when the power button is double tapped on the LOCKSCREEN (the
+     * first tap transitions to AOD, the second cancels that transition and starts AOD -> OCCLUDED.
+     */
+    fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var currentAlpha = 0f
+        return transitionAnimation.sharedFlow(
+            duration = 250.milliseconds,
+            startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements.
+            onStart = { currentAlpha = viewState.alpha() },
+            onStep = { MathUtils.lerp(currentAlpha, 0f, it) },
+            onCancel = { 0f },
+        )
+    }
+
     override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt
new file mode 100644
index 0000000..c0b1195
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 android.util.MathUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down DOZING->OCCLUDED transition into discrete steps for corresponding views to consume.
+ */
+@SysUISingleton
+class DozingToOccludedTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION,
+            from = KeyguardState.DOZING,
+            to = KeyguardState.OCCLUDED,
+        )
+
+    /**
+     * Fade out the lockscreen during a transition to OCCLUDED.
+     *
+     * This happens when pressing the power button while a SHOW_WHEN_LOCKED activity is on the top
+     * of the task stack, as well as when the power button is double tapped on the LOCKSCREEN (the
+     * first tap transitions to DOZING, the second cancels that transition and starts DOZING ->
+     * OCCLUDED.
+     */
+    fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var currentAlpha = 0f
+        return transitionAnimation.sharedFlow(
+            duration = 250.milliseconds,
+            startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements.
+            onStart = { currentAlpha = viewState.alpha() },
+            onStep = { MathUtils.lerp(currentAlpha, 0f, it) },
+            onCancel = { 0f },
+        )
+    }
+
+    override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f)
+}
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 1760b92..f848717 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
@@ -73,8 +73,10 @@
         AlternateBouncerToGoneTransitionViewModel,
     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+    private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
     private val dozingToGoneTransitionViewModel: DozingToGoneTransitionViewModel,
     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
+    private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
     private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
     private val glanceableHubToLockscreenTransitionViewModel:
         GlanceableHubToLockscreenTransitionViewModel,
@@ -170,8 +172,10 @@
                         alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
+                        aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                         dozingToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         dozingToLockscreenTransitionViewModel.lockscreenAlpha,
+                        dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                         dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
                         glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
                         goneToAodTransitionViewModel.enterFromTopAnimationAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 840b309..26c63f3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -109,7 +109,7 @@
 import com.android.systemui.media.controls.util.MediaFlags;
 import com.android.systemui.media.controls.util.MediaUiEventLogger;
 import com.android.systemui.media.controls.util.SmallHash;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.monet.ColorScheme;
 import com.android.systemui.monet.Style;
 import com.android.systemui.plugins.ActivityStarter;
@@ -223,7 +223,7 @@
     protected int mUid = Process.INVALID_UID;
     private int mSmartspaceMediaItemsCount;
     private MediaCarouselController mMediaCarouselController;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final MediaOutputDialogManager mMediaOutputDialogManager;
     private final FalsingManager mFalsingManager;
     private MetadataAnimationHandler mMetadataAnimationHandler;
     private ColorSchemeTransition mColorSchemeTransition;
@@ -304,7 +304,7 @@
             MediaViewController mediaViewController,
             SeekBarViewModel seekBarViewModel,
             Lazy<MediaDataManager> lazyMediaDataManager,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaOutputDialogManager mediaOutputDialogManager,
             MediaCarouselController mediaCarouselController,
             FalsingManager falsingManager,
             SystemClock systemClock,
@@ -324,7 +324,7 @@
         mSeekBarViewModel = seekBarViewModel;
         mMediaViewController = mediaViewController;
         mMediaDataManagerLazy = lazyMediaDataManager;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaOutputDialogManager = mediaOutputDialogManager;
         mMediaCarouselController = mediaCarouselController;
         mFalsingManager = falsingManager;
         mSystemClock = systemClock;
@@ -737,7 +737,7 @@
                                     mPackageName, mMediaViewHolder.getSeamlessButton());
                         } else {
                             mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
-                            mMediaOutputDialogFactory.create(mPackageName, true,
+                            mMediaOutputDialogManager.createAndShow(mPackageName, true,
                                     mMediaViewHolder.getSeamlessButton());
                         }
                     } else {
@@ -761,7 +761,7 @@
                                 }
                             }
                         } else {
-                            mMediaOutputDialogFactory.create(mPackageName, true,
+                            mMediaOutputDialogManager.createAndShow(mPackageName, true,
                                     mMediaViewHolder.getSeamlessButton());
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt
index 5d113a9..452cb7e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/LocalMediaManagerFactory.kt
@@ -31,7 +31,8 @@
 ) {
     /** Creates a [LocalMediaManager] for the given package. */
     fun create(packageName: String?): LocalMediaManager {
-        return InfoMediaManager.createInstance(context, packageName, null, localBluetoothManager)
-            .run { LocalMediaManager(context, localBluetoothManager, this, packageName) }
+        return InfoMediaManager.createInstance(context, packageName, localBluetoothManager).run {
+            LocalMediaManager(context, localBluetoothManager, this, packageName)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
deleted file mode 100644
index b6e3937..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
+++ /dev/null
@@ -1,83 +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.systemui.media.dialog
-
-import android.app.KeyguardManager
-import android.content.Context
-import android.media.AudioManager
-import android.media.session.MediaSessionManager
-import android.os.PowerExemptionManager
-import android.view.View
-import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.media.nearby.NearbyMediaDevicesManager
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import javax.inject.Inject
-
-/**
- * Factory to create [MediaOutputBroadcastDialog] objects.
- */
-class MediaOutputBroadcastDialogFactory @Inject constructor(
-    private val context: Context,
-    private val mediaSessionManager: MediaSessionManager,
-    private val lbm: LocalBluetoothManager?,
-    private val starter: ActivityStarter,
-    private val broadcastSender: BroadcastSender,
-    private val notifCollection: CommonNotifCollection,
-    private val uiEventLogger: UiEventLogger,
-    private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val nearbyMediaDevicesManager: NearbyMediaDevicesManager,
-    private val audioManager: AudioManager,
-    private val powerExemptionManager: PowerExemptionManager,
-    private val keyGuardManager: KeyguardManager,
-    private val featureFlags: FeatureFlags,
-    private val userTracker: UserTracker
-) {
-    var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
-
-    /** Creates a [MediaOutputBroadcastDialog] for the given package. */
-    fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
-        // Dismiss the previous dialog, if any.
-        mediaOutputBroadcastDialog?.dismiss()
-
-        val controller = MediaOutputController(context, packageName,
-                mediaSessionManager, lbm, starter, notifCollection,
-                dialogTransitionAnimator, nearbyMediaDevicesManager, audioManager,
-                powerExemptionManager, keyGuardManager, featureFlags, userTracker)
-        val dialog =
-                MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
-        mediaOutputBroadcastDialog = dialog
-
-        // Show the dialog.
-        if (view != null) {
-            dialogTransitionAnimator.showFromView(dialog, view)
-        } else {
-            dialog.show()
-        }
-    }
-
-    /** dismiss [MediaOutputBroadcastDialog] if exist. */
-    fun dismiss() {
-        mediaOutputBroadcastDialog?.dismiss()
-        mediaOutputBroadcastDialog = null
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
new file mode 100644
index 0000000..54d175c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.systemui.media.dialog
+
+import android.content.Context
+import android.view.View
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import javax.inject.Inject
+
+/** Manager to create and show a [MediaOutputBroadcastDialog]. */
+class MediaOutputBroadcastDialogManager
+@Inject
+constructor(
+    private val context: Context,
+    private val broadcastSender: BroadcastSender,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
+    private val mediaOutputControllerFactory: MediaOutputController.Factory
+) {
+    var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
+
+    /** Creates a [MediaOutputBroadcastDialog] for the given package. */
+    fun createAndShow(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
+        // Dismiss the previous dialog, if any.
+        mediaOutputBroadcastDialog?.dismiss()
+
+        val controller = mediaOutputControllerFactory.create(packageName)
+        val dialog =
+            MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
+        mediaOutputBroadcastDialog = dialog
+
+        // Show the dialog.
+        if (view != null) {
+            dialogTransitionAnimator.showFromView(dialog, view)
+        } else {
+            dialog.show()
+        }
+    }
+
+    /** dismiss [MediaOutputBroadcastDialog] if exist. */
+    fun dismiss() {
+        mediaOutputBroadcastDialog?.dismiss()
+        mediaOutputBroadcastDialog = null
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index b3b7bce..adee7f2c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -64,6 +64,7 @@
 import android.view.WindowManager;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.core.graphics.drawable.IconCompat;
 
@@ -91,6 +92,10 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -105,8 +110,6 @@
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
-import javax.inject.Inject;
-
 /**
  * Controller for media output dialog
  */
@@ -170,10 +173,13 @@
         ACTION_BROADCAST_INFO_ICON
     }
 
-    @Inject
-    public MediaOutputController(@NonNull Context context, String packageName,
-            MediaSessionManager mediaSessionManager, LocalBluetoothManager
-            lbm, ActivityStarter starter,
+    @AssistedInject
+    public MediaOutputController(
+            Context context,
+            @Assisted String packageName,
+            MediaSessionManager mediaSessionManager,
+            @Nullable LocalBluetoothManager lbm,
+            ActivityStarter starter,
             CommonNotifCollection notifCollection,
             DialogTransitionAnimator dialogTransitionAnimator,
             NearbyMediaDevicesManager nearbyMediaDevicesManager,
@@ -193,7 +199,7 @@
         mKeyGuardManager = keyGuardManager;
         mFeatureFlags = featureFlags;
         mUserTracker = userTracker;
-        InfoMediaManager imm = InfoMediaManager.createInstance(mContext, packageName, null, lbm);
+        InfoMediaManager imm = InfoMediaManager.createInstance(mContext, packageName, lbm);
         mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
         mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
         mDialogTransitionAnimator = dialogTransitionAnimator;
@@ -222,6 +228,12 @@
                 R.dimen.media_output_dialog_selectable_margin_end);
     }
 
+    @AssistedFactory
+    public interface Factory {
+        /** Construct a MediaOutputController */
+        MediaOutputController create(String packageName);
+    }
+
     protected void start(@NonNull Callback cb) {
         synchronized (mMediaDevicesLock) {
             mCachedMediaDevices.clear();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
similarity index 62%
rename from packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
rename to packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
index 02be0c1..e7816a4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
@@ -16,43 +16,24 @@
 
 package com.android.systemui.media.dialog
 
-import android.app.KeyguardManager
 import android.content.Context
-import android.media.AudioManager
-import android.media.session.MediaSessionManager
-import android.os.PowerExemptionManager
 import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.media.nearby.NearbyMediaDevicesManager
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import javax.inject.Inject
 
-/** Factory to create [MediaOutputDialog] objects. */
-open class MediaOutputDialogFactory
+/** Manager to create and show a [MediaOutputDialog]. */
+open class MediaOutputDialogManager
 @Inject
 constructor(
     private val context: Context,
-    private val mediaSessionManager: MediaSessionManager,
-    private val lbm: LocalBluetoothManager?,
-    private val starter: ActivityStarter,
     private val broadcastSender: BroadcastSender,
-    private val notifCollection: CommonNotifCollection,
     private val uiEventLogger: UiEventLogger,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val nearbyMediaDevicesManager: NearbyMediaDevicesManager,
-    private val audioManager: AudioManager,
-    private val powerExemptionManager: PowerExemptionManager,
-    private val keyGuardManager: KeyguardManager,
-    private val featureFlags: FeatureFlags,
-    private val userTracker: UserTracker
+    private val mediaOutputControllerFactory: MediaOutputController.Factory,
 ) {
     companion object {
         const val INTERACTION_JANK_TAG = "media_output"
@@ -60,8 +41,8 @@
     }
 
     /** Creates a [MediaOutputDialog] for the given package. */
-    open fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
-        createWithController(
+    open fun createAndShow(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
+        createAndShowWithController(
             packageName,
             aboveStatusBar,
             controller =
@@ -78,12 +59,12 @@
     }
 
     /** Creates a [MediaOutputDialog] for the given package. */
-    open fun createWithController(
+    open fun createAndShowWithController(
         packageName: String,
         aboveStatusBar: Boolean,
         controller: DialogTransitionAnimator.Controller?,
     ) {
-        create(
+        createAndShow(
             packageName,
             aboveStatusBar,
             dialogTransitionAnimatorController = controller,
@@ -91,8 +72,10 @@
         )
     }
 
-    open fun createDialogForSystemRouting(controller: DialogTransitionAnimator.Controller? = null) {
-        create(
+    open fun createAndShowForSystemRouting(
+        controller: DialogTransitionAnimator.Controller? = null
+    ) {
+        createAndShow(
             packageName = null,
             aboveStatusBar = false,
             dialogTransitionAnimatorController = null,
@@ -100,7 +83,7 @@
         )
     }
 
-    private fun create(
+    private fun createAndShow(
         packageName: String?,
         aboveStatusBar: Boolean,
         dialogTransitionAnimatorController: DialogTransitionAnimator.Controller?,
@@ -109,23 +92,9 @@
         // Dismiss the previous dialog, if any.
         mediaOutputDialog?.dismiss()
 
-        val controller =
-            MediaOutputController(
-                context,
-                packageName,
-                mediaSessionManager,
-                lbm,
-                starter,
-                notifCollection,
-                dialogTransitionAnimator,
-                nearbyMediaDevicesManager,
-                audioManager,
-                powerExemptionManager,
-                keyGuardManager,
-                featureFlags,
-                userTracker
-            )
-        val dialog =
+        val controller = mediaOutputControllerFactory.create(packageName)
+
+        val mediaOutputDialog =
             MediaOutputDialog(
                 context,
                 aboveStatusBar,
@@ -135,16 +104,15 @@
                 uiEventLogger,
                 includePlaybackAndAppMetadata
             )
-        mediaOutputDialog = dialog
 
         // Show the dialog.
         if (dialogTransitionAnimatorController != null) {
             dialogTransitionAnimator.show(
-                dialog,
+                mediaOutputDialog,
                 dialogTransitionAnimatorController,
             )
         } else {
-            dialog.show()
+            mediaOutputDialog.show()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
index 38d31ed..774792f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
@@ -31,8 +31,8 @@
  * BroadcastReceiver for handling media output intent
  */
 class MediaOutputDialogReceiver @Inject constructor(
-    private val mediaOutputDialogFactory: MediaOutputDialogFactory,
-    private val mediaOutputBroadcastDialogFactory: MediaOutputBroadcastDialogFactory
+        private val mediaOutputDialogManager: MediaOutputDialogManager,
+        private val mediaOutputBroadcastDialogManager: MediaOutputBroadcastDialogManager
 ) : BroadcastReceiver() {
     override fun onReceive(context: Context, intent: Intent) {
         when (intent.action) {
@@ -42,7 +42,7 @@
                 launchMediaOutputDialogIfPossible(packageName)
             }
             MediaOutputConstants.ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG -> {
-                mediaOutputDialogFactory.createDialogForSystemRouting()
+                mediaOutputDialogManager.createAndShowForSystemRouting()
             }
             MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG -> {
                 if (!legacyLeAudioSharing()) return
@@ -55,7 +55,7 @@
 
     private fun launchMediaOutputDialogIfPossible(packageName: String?) {
         if (!packageName.isNullOrEmpty()) {
-            mediaOutputDialogFactory.create(packageName, false)
+            mediaOutputDialogManager.createAndShow(packageName, false)
         } else if (DEBUG) {
             Log.e(TAG, "Unable to launch media output dialog. Package name is empty.")
         }
@@ -63,7 +63,7 @@
 
     private fun launchMediaOutputBroadcastDialogIfPossible(packageName: String?) {
         if (!packageName.isNullOrEmpty()) {
-            mediaOutputBroadcastDialogFactory.create(
+            mediaOutputBroadcastDialogManager.createAndShow(
                     packageName, aboveStatusBar = true, view = null)
         } else if (DEBUG) {
             Log.e(TAG, "Unable to launch media output broadcast dialog. Package name is empty.")
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
index b5b1f0f..6e7e0f2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
@@ -34,15 +34,15 @@
     private static final String TAG = "MediaOutputSwitcherDialogUI";
 
     private final CommandQueue mCommandQueue;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final MediaOutputDialogManager mMediaOutputDialogManager;
 
     @Inject
     public MediaOutputSwitcherDialogUI(
             Context context,
             CommandQueue commandQueue,
-            MediaOutputDialogFactory mediaOutputDialogFactory) {
+            MediaOutputDialogManager mediaOutputDialogManager) {
         mCommandQueue = commandQueue;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaOutputDialogManager = mediaOutputDialogManager;
     }
 
     @Override
@@ -54,7 +54,7 @@
     @MainThread
     public void showMediaOutputSwitcher(String packageName) {
         if (!TextUtils.isEmpty(packageName)) {
-            mMediaOutputDialogFactory.create(packageName, false, null);
+            mMediaOutputDialogManager.createAndShow(packageName, false, null);
         } else {
             Log.e(TAG, "Unable to launch media output dialog. Package name is empty.");
         }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index c0e688f..c7aae3c 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -23,6 +23,7 @@
 import android.app.role.RoleManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.notetask.NoteTaskBubblesController.NoteTaskBubblesService
 import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule
 import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
 import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
@@ -37,20 +38,21 @@
 interface NoteTaskModule {
 
     @[Binds IntoMap ClassKey(NoteTaskControllerUpdateService::class)]
-    fun NoteTaskControllerUpdateService.bindNoteTaskControllerUpdateService(): Service
+    fun bindNoteTaskControllerUpdateService(service: NoteTaskControllerUpdateService): Service
 
-    @[Binds IntoMap ClassKey(NoteTaskBubblesController.NoteTaskBubblesService::class)]
-    fun NoteTaskBubblesController.NoteTaskBubblesService.bindNoteTaskBubblesService(): Service
+    @[Binds IntoMap ClassKey(NoteTaskBubblesService::class)]
+    fun bindNoteTaskBubblesService(service: NoteTaskBubblesService): Service
 
     @[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
-    fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity
+    fun bindNoteTaskLauncherActivity(activity: LaunchNoteTaskActivity): Activity
 
     @[Binds IntoMap ClassKey(LaunchNotesRoleSettingsTrampolineActivity::class)]
-    fun LaunchNotesRoleSettingsTrampolineActivity.bindLaunchNotesRoleSettingsTrampolineActivity():
-        Activity
+    fun bindLaunchNotesRoleSettingsTrampolineActivity(
+        activity: LaunchNotesRoleSettingsTrampolineActivity
+    ): Activity
 
     @[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)]
-    fun CreateNoteTaskShortcutActivity.bindNoteTaskShortcutActivity(): Activity
+    fun bindNoteTaskShortcutActivity(activity: CreateNoteTaskShortcutActivity): Activity
 
     companion object {
 
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
index 2d63dbc..7d3f2a5 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
@@ -25,5 +25,7 @@
 interface NoteTaskQuickAffordanceModule {
 
     @[Binds IntoSet]
-    fun NoteTaskQuickAffordanceConfig.bindNoteTaskQuickAffordance(): KeyguardQuickAffordanceConfig
+    fun bindNoteTaskQuickAffordance(
+        impl: NoteTaskQuickAffordanceConfig
+    ): KeyguardQuickAffordanceConfig
 }
diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
index e1d1ec2..5432793d 100644
--- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
@@ -20,20 +20,24 @@
      * the [KeyguardTransitionInteractor].
      */
     internal val internalWakefulnessState: WakefulnessState = WakefulnessState.AWAKE,
-
     val lastWakeReason: WakeSleepReason = WakeSleepReason.OTHER,
     val lastSleepReason: WakeSleepReason = WakeSleepReason.OTHER,
 
-        /**
+    /**
      * Whether the power button double tap gesture was triggered since the last time went to sleep.
      * If this value is true while [isAsleep]=true, it means we'll be waking back up shortly. If it
      * is true while [isAwake]=true, it means we're awake because of the button gesture.
      *
-     * This value remains true until the next time [isAsleep]=true.
+     * This value remains true until the next time [isAsleep]=true, since it would otherwise be
+     * totally arbitrary at what point we decide the gesture was no longer "triggered". Since a
+     * sleep event is guaranteed to arrive prior to the next power button gesture (as the first tap
+     * of the double tap always begins a sleep transition), this will always be reset to false prior
+     * to a subsequent power gesture.
      */
     val powerButtonLaunchGestureTriggered: Boolean = false,
 ) {
-    fun isAwake() = internalWakefulnessState == WakefulnessState.AWAKE ||
+    fun isAwake() =
+        internalWakefulnessState == WakefulnessState.AWAKE ||
             internalWakefulnessState == WakefulnessState.STARTING_TO_WAKE
 
     fun isAsleep() = !isAwake()
@@ -48,11 +52,10 @@
     fun isAsleepFrom(wakeSleepReason: WakeSleepReason) =
         isAsleep() && lastSleepReason == wakeSleepReason
 
-    fun isAwakeOrAsleepFrom(reason: WakeSleepReason) =
-        isAsleepFrom(reason) || isAwakeFrom(reason)
+    fun isAwakeOrAsleepFrom(reason: WakeSleepReason) = isAsleepFrom(reason) || isAwakeFrom(reason)
 
     fun isAwakeFromTapOrGesture(): Boolean {
-        return isAwake() && (lastWakeReason == WakeSleepReason.TAP ||
-                lastWakeReason == WakeSleepReason.GESTURE)
+        return isAwake() &&
+            (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileDataInteractor.kt
new file mode 100644
index 0000000..22bbbbb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileDataInteractor.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.util.kotlin.combine
+import com.android.systemui.util.kotlin.getBatteryLevel
+import com.android.systemui.util.kotlin.isBatteryPowerSaveEnabled
+import com.android.systemui.util.kotlin.isDevicePluggedIn
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+
+/** Observes BatterySaver mode state changes providing the [BatterySaverTileModel.Standard]. */
+open class BatterySaverTileDataInteractor
+@Inject
+constructor(
+    @Background private val bgCoroutineContext: CoroutineContext,
+    private val batteryController: BatteryController,
+) : QSTileDataInteractor<BatterySaverTileModel> {
+
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<BatterySaverTileModel> =
+        combine(
+            batteryController.isDevicePluggedIn().distinctUntilChanged().flowOn(bgCoroutineContext),
+            batteryController
+                .isBatteryPowerSaveEnabled()
+                .distinctUntilChanged()
+                .flowOn(bgCoroutineContext),
+            batteryController.getBatteryLevel().distinctUntilChanged().flowOn(bgCoroutineContext),
+        ) {
+            isPluggedIn: Boolean,
+            isPowerSaverEnabled: Boolean,
+            _, // we are only interested in battery level change, not the actual level
+            ->
+            BatterySaverTileModel.Standard(isPluggedIn, isPowerSaverEnabled)
+        }
+
+    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
new file mode 100644
index 0000000..1e4eb38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.policy.BatteryController
+import javax.inject.Inject
+
+/** Handles airplane mode tile clicks and long clicks. */
+class BatterySaverTileUserActionInteractor
+@Inject
+constructor(
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+    private val batteryController: BatteryController
+) : QSTileUserActionInteractor<BatterySaverTileModel> {
+
+    override suspend fun handleInput(input: QSTileInput<BatterySaverTileModel>) =
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    if (!data.isPluggedIn) {
+                        batteryController.setPowerSaveMode(!data.isPowerSaving, action.view)
+                    }
+                }
+                is QSTileUserAction.LongClick -> {
+                    qsTileIntentUserActionHandler.handle(
+                        action.view,
+                        Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)
+                    )
+                }
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/model/BatterySaverTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/model/BatterySaverTileModel.kt
new file mode 100644
index 0000000..dbec50d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/model/BatterySaverTileModel.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.qs.tiles.impl.battery.domain.model
+
+/** BatterySaver mode tile model. */
+sealed interface BatterySaverTileModel {
+
+    val isPluggedIn: Boolean
+    val isPowerSaving: Boolean
+
+    /** For when the device does not support extreme battery saver mode. */
+    data class Standard(
+        override val isPluggedIn: Boolean,
+        override val isPowerSaving: Boolean,
+    ) : BatterySaverTileModel
+
+    /**
+     * For when device supports extreme battery saver mode. Whether or not that mode is enabled is
+     * determined through [isExtremeSaving].
+     */
+    data class Extreme(
+        override val isPluggedIn: Boolean,
+        override val isPowerSaving: Boolean,
+        val isExtremeSaving: Boolean,
+    ) : BatterySaverTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt
new file mode 100644
index 0000000..0c08fba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery.ui
+
+import android.content.res.Resources
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.battery.domain.model.BatterySaverTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [BatterySaverTileModel] to [QSTileState]. */
+open class BatterySaverTileMapper
+@Inject
+constructor(
+    @Main protected val resources: Resources,
+    private val theme: Resources.Theme,
+) : QSTileDataToStateMapper<BatterySaverTileModel> {
+
+    override fun map(config: QSTileConfig, data: BatterySaverTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            label = resources.getString(R.string.battery_detail_switch_title)
+            contentDescription = label
+
+            icon = {
+                Icon.Loaded(
+                    resources.getDrawable(
+                        if (data.isPowerSaving) R.drawable.qs_battery_saver_icon_on
+                        else R.drawable.qs_battery_saver_icon_off,
+                        theme
+                    ),
+                    null
+                )
+            }
+
+            sideViewIcon = QSTileState.SideViewIcon.None
+
+            if (data.isPluggedIn) {
+                activationState = QSTileState.ActivationState.UNAVAILABLE
+                supportedActions = setOf(QSTileState.UserAction.LONG_CLICK)
+                secondaryLabel = ""
+            } else if (data.isPowerSaving) {
+                activationState = QSTileState.ActivationState.ACTIVE
+                supportedActions =
+                    setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+
+                if (data is BatterySaverTileModel.Extreme) {
+                    secondaryLabel =
+                        resources.getString(
+                            if (data.isExtremeSaving) R.string.extreme_battery_saver_text
+                            else R.string.standard_battery_saver_text
+                        )
+                    stateDescription = secondaryLabel
+                } else {
+                    secondaryLabel = ""
+                }
+            } else {
+                activationState = QSTileState.ActivationState.INACTIVE
+                supportedActions =
+                    setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+                secondaryLabel = ""
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
new file mode 100644
index 0000000..caae4d2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain
+
+import android.content.Context
+import android.content.res.Resources
+import android.widget.Switch
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [InternetTileModel] to [QSTileState]. */
+class InternetTileMapper
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val theme: Resources.Theme,
+    private val context: Context,
+) : QSTileDataToStateMapper<InternetTileModel> {
+
+    override fun map(config: QSTileConfig, data: InternetTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            label = resources.getString(R.string.quick_settings_internet_label)
+            expandedAccessibilityClass = Switch::class
+
+            if (data.secondaryLabel != null) {
+                secondaryLabel = data.secondaryLabel.loadText(context)
+            } else {
+                secondaryLabel = data.secondaryTitle
+            }
+
+            stateDescription = data.stateDescription.loadContentDescription(context)
+            contentDescription = data.contentDescription.loadContentDescription(context)
+
+            if (data.icon != null) {
+                this.icon = { data.icon }
+            } else if (data.iconId != null) {
+                val loadedIcon =
+                    Icon.Loaded(
+                        resources.getDrawable(data.iconId!!, theme),
+                        contentDescription = null
+                    )
+                this.icon = { loadedIcon }
+            }
+
+            sideViewIcon = QSTileState.SideViewIcon.Chevron
+
+            activationState =
+                if (data is InternetTileModel.Active) QSTileState.ActivationState.ACTIVE
+                else QSTileState.ActivationState.INACTIVE
+
+            supportedActions =
+                setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
new file mode 100644
index 0000000..fdc596b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet.domain.interactor
+
+import android.annotation.StringRes
+import android.content.Context
+import android.os.UserHandle
+import android.text.Html
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+/** Observes internet state changes providing the [InternetTileModel]. */
+class InternetTileDataInteractor
+@Inject
+constructor(
+    private val context: Context,
+    @Application private val scope: CoroutineScope,
+    airplaneModeRepository: AirplaneModeRepository,
+    private val connectivityRepository: ConnectivityRepository,
+    ethernetInteractor: EthernetInteractor,
+    mobileIconsInteractor: MobileIconsInteractor,
+    wifiInteractor: WifiInteractor,
+) : QSTileDataInteractor<InternetTileModel> {
+    private val internetLabel: String = context.getString(R.string.quick_settings_internet_label)
+
+    // Three symmetrical Flows that can be switched upon based on the value of
+    // [DefaultConnectionModel]
+    private val wifiIconFlow: Flow<InternetTileModel> =
+        wifiInteractor.wifiNetwork.flatMapLatest {
+            val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true)
+            if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) {
+                val secondary = removeDoubleQuotes(it.ssid)
+                flowOf(
+                    InternetTileModel.Active(
+                        secondaryTitle = secondary,
+                        icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null),
+                        stateDescription = wifiIcon.contentDescription,
+                        contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"),
+                    )
+                )
+            } else {
+                notConnectedFlow
+            }
+        }
+
+    private val mobileDataContentName: Flow<CharSequence?> =
+        mobileIconsInteractor.activeDataIconInteractor.flatMapLatest {
+            if (it == null) {
+                flowOf(null)
+            } else {
+                combine(it.isRoaming, it.networkTypeIconGroup) { isRoaming, networkTypeIconGroup ->
+                    val cd = loadString(networkTypeIconGroup.contentDescription)
+                    if (isRoaming) {
+                        val roaming = context.getString(R.string.data_connection_roaming)
+                        if (cd != null) {
+                            context.getString(R.string.mobile_data_text_format, roaming, cd)
+                        } else {
+                            roaming
+                        }
+                    } else {
+                        cd
+                    }
+                }
+            }
+        }
+
+    private val mobileIconFlow: Flow<InternetTileModel> =
+        mobileIconsInteractor.activeDataIconInteractor.flatMapLatest {
+            if (it == null) {
+                notConnectedFlow
+            } else {
+                combine(
+                    it.networkName,
+                    it.signalLevelIcon,
+                    mobileDataContentName,
+                ) { networkNameModel, signalIcon, dataContentDescription ->
+                    when (signalIcon) {
+                        is SignalIconModel.Cellular -> {
+                            val secondary =
+                                mobileDataContentConcat(
+                                    networkNameModel.name,
+                                    dataContentDescription
+                                )
+
+                            val stateLevel = signalIcon.level
+                            val drawable = SignalDrawable(context)
+                            drawable.setLevel(stateLevel)
+                            val loadedIcon = Icon.Loaded(drawable, null)
+
+                            InternetTileModel.Active(
+                                secondaryTitle = secondary,
+                                icon = loadedIcon,
+                                stateDescription = ContentDescription.Loaded(secondary.toString()),
+                                contentDescription = ContentDescription.Loaded(internetLabel),
+                            )
+                        }
+                        is SignalIconModel.Satellite -> {
+                            val secondary =
+                                signalIcon.icon.contentDescription.loadContentDescription(context)
+                            InternetTileModel.Active(
+                                secondaryTitle = secondary,
+                                iconId = signalIcon.icon.res,
+                                stateDescription = ContentDescription.Loaded(secondary),
+                                contentDescription = ContentDescription.Loaded(internetLabel),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+    private fun mobileDataContentConcat(
+        networkName: String?,
+        dataContentDescription: CharSequence?
+    ): CharSequence {
+        if (dataContentDescription == null) {
+            return networkName ?: ""
+        }
+        if (networkName == null) {
+            return Html.fromHtml(dataContentDescription.toString(), 0)
+        }
+
+        return Html.fromHtml(
+            context.getString(
+                R.string.mobile_carrier_text_format,
+                networkName,
+                dataContentDescription
+            ),
+            0
+        )
+    }
+
+    private fun loadString(@StringRes resId: Int): CharSequence? =
+        if (resId != 0) {
+            context.getString(resId)
+        } else {
+            null
+        }
+
+    private val ethernetIconFlow: Flow<InternetTileModel> =
+        ethernetInteractor.icon.flatMapLatest {
+            if (it == null) {
+                notConnectedFlow
+            } else {
+                val secondary = it.contentDescription
+                flowOf(
+                    InternetTileModel.Active(
+                        secondaryLabel = secondary?.toText(),
+                        iconId = it.res,
+                        stateDescription = null,
+                        contentDescription = secondary,
+                    )
+                )
+            }
+        }
+
+    private val notConnectedFlow: StateFlow<InternetTileModel> =
+        combine(
+                wifiInteractor.areNetworksAvailable,
+                airplaneModeRepository.isAirplaneMode,
+            ) { networksAvailable, isAirplaneMode ->
+                when {
+                    isAirplaneMode -> {
+                        val secondary = context.getString(R.string.status_bar_airplane)
+                        InternetTileModel.Inactive(
+                            secondaryTitle = secondary,
+                            iconId = R.drawable.ic_qs_no_internet_unavailable,
+                            stateDescription = null,
+                            contentDescription = ContentDescription.Loaded(secondary),
+                        )
+                    }
+                    networksAvailable -> {
+                        val secondary =
+                            context.getString(R.string.quick_settings_networks_available)
+                        InternetTileModel.Inactive(
+                            secondaryTitle = secondary,
+                            iconId = R.drawable.ic_qs_no_internet_available,
+                            stateDescription = null,
+                            contentDescription =
+                                ContentDescription.Loaded("$internetLabel,$secondary")
+                        )
+                    }
+                    else -> {
+                        NOT_CONNECTED_NETWORKS_UNAVAILABLE
+                    }
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), NOT_CONNECTED_NETWORKS_UNAVAILABLE)
+
+    /**
+     * Consumable flow describing the correct state for the InternetTile.
+     *
+     * Strict ordering of which repo is sending its data to the internet tile. Swaps between each of
+     * the interim providers (wifi, mobile, ethernet, or not-connected).
+     */
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<InternetTileModel> =
+        connectivityRepository.defaultConnections.flatMapLatest {
+            when {
+                it.ethernet.isDefault -> ethernetIconFlow
+                it.mobile.isDefault || it.carrierMerged.isDefault -> mobileIconFlow
+                it.wifi.isDefault -> wifiIconFlow
+                else -> notConnectedFlow
+            }
+        }
+
+    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
+
+    private companion object {
+        val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
+            InternetTileModel.Inactive(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+            )
+
+        fun removeDoubleQuotes(string: String?): String? {
+            if (string == null) return null
+            return if (string.firstOrNull() == '"' && string.lastOrNull() == '"') {
+                string.substring(1, string.length - 1)
+            } else string
+        }
+
+        fun ContentDescription.toText(): Text =
+            when (this) {
+                is ContentDescription.Loaded -> Text.Loaded(this.description)
+                is ContentDescription.Resource -> Text.Resource(this.res)
+            }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
new file mode 100644
index 0000000..2620cd5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.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.qs.tiles.impl.internet.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.connectivity.AccessPointController
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+/** Handles internet tile clicks. */
+class InternetTileUserActionInteractor
+@Inject
+constructor(
+    @Main private val mainContext: CoroutineContext,
+    private val internetDialogManager: InternetDialogManager,
+    private val accessPointController: AccessPointController,
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+) : QSTileUserActionInteractor<InternetTileModel> {
+
+    override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit =
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    withContext(mainContext) {
+                        internetDialogManager.create(
+                            aboveStatusBar = true,
+                            accessPointController.canConfigMobileData(),
+                            accessPointController.canConfigWifi(),
+                            action.view,
+                        )
+                    }
+                }
+                is QSTileUserAction.LongClick -> {
+                    qsTileIntentUserActionHandler.handle(
+                        action.view,
+                        Intent(Settings.ACTION_WIFI_SETTINGS)
+                    )
+                }
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
new file mode 100644
index 0000000..ece90461
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.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.qs.tiles.impl.internet.domain.model
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+
+/** Model describing the state that the QS Internet tile should be in. */
+sealed interface InternetTileModel {
+    val secondaryTitle: CharSequence?
+    val secondaryLabel: Text?
+    val iconId: Int?
+    val icon: Icon?
+    val stateDescription: ContentDescription?
+    val contentDescription: ContentDescription?
+
+    data class Active(
+        override val secondaryTitle: CharSequence? = null,
+        override val secondaryLabel: Text? = null,
+        override val iconId: Int? = null,
+        override val icon: Icon? = null,
+        override val stateDescription: ContentDescription? = null,
+        override val contentDescription: ContentDescription? = null,
+    ) : InternetTileModel
+
+    data class Inactive(
+        override val secondaryTitle: CharSequence? = null,
+        override val secondaryLabel: Text? = null,
+        override val iconId: Int? = null,
+        override val icon: Icon? = null,
+        override val stateDescription: ContentDescription? = null,
+        override val contentDescription: ContentDescription? = null,
+    ) : InternetTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
index d0585d3..20bd7c6 100644
--- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
@@ -64,8 +64,6 @@
     private String mScrimName;
     private int mTintColor;
     private boolean mBlendWithMainColor = true;
-    private Runnable mChangeRunnable;
-    private Executor mChangeRunnableExecutor;
     private Executor mExecutor;
     private Looper mExecutorLooper;
     @Nullable
@@ -270,9 +268,6 @@
             mDrawable.invalidateSelf();
         }
 
-        if (mChangeRunnable != null) {
-            mChangeRunnableExecutor.execute(mChangeRunnable);
-        }
     }
 
     public int getTint() {
@@ -300,9 +295,6 @@
                 mViewAlpha = alpha;
 
                 mDrawable.setAlpha((int) (255 * alpha));
-                if (mChangeRunnable != null) {
-                    mChangeRunnableExecutor.execute(mChangeRunnable);
-                }
             }
         });
     }
@@ -311,14 +303,6 @@
         return mViewAlpha;
     }
 
-    /**
-     * Sets a callback that is invoked whenever the alpha, color, or tint change.
-     */
-    public void setChangeRunnable(Runnable changeRunnable, Executor changeRunnableExecutor) {
-        mChangeRunnable = changeRunnable;
-        mChangeRunnableExecutor = changeRunnableExecutor;
-    }
-
     @Override
     protected boolean canReceivePointerEvents() {
         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index ef4e530..a12b970 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -84,6 +84,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -457,14 +458,43 @@
         List<KeyboardShortcutMultiMappingGroup> keyboardShortcutMultiMappingGroups =
                 new ArrayList<>();
         for (KeyboardShortcutGroup group : keyboardShortcutGroups) {
-            CharSequence categoryTitle = group.getLabel();
-            List<ShortcutMultiMappingInfo> shortcutMultiMappingInfos = new ArrayList<>();
-            for (KeyboardShortcutInfo info : group.getItems()) {
-                shortcutMultiMappingInfos.add(new ShortcutMultiMappingInfo(info));
-            }
-            keyboardShortcutMultiMappingGroups.add(
+            KeyboardShortcutMultiMappingGroup mappedGroup =
                     new KeyboardShortcutMultiMappingGroup(
-                            categoryTitle, shortcutMultiMappingInfos));
+                            group.getLabel(),
+                            new ArrayList<>());
+            Map<String, List<ShortcutMultiMappingInfo>> shortcutMap = new LinkedHashMap<>();
+            for (KeyboardShortcutInfo info : group.getItems()) {
+                String label = info.getLabel().toString();
+                Icon icon = info.getIcon();
+                if (shortcutMap.containsKey(label)) {
+                    List<ShortcutMultiMappingInfo> shortcuts = shortcutMap.get(label);
+                    boolean foundSameIcon = false;
+                    for (ShortcutMultiMappingInfo shortcut : shortcuts) {
+                        Icon shortcutIcon = shortcut.getIcon();
+                        if ((shortcutIcon != null
+                                && icon != null
+                                && shortcutIcon.sameAs(icon))
+                                || (shortcutIcon == null && icon == null)) {
+                            foundSameIcon = true;
+                            shortcut.addShortcutKeyGroup(new ShortcutKeyGroup(info, null));
+                            break;
+                        }
+                    }
+                    if (!foundSameIcon) {
+                        shortcuts.add(new ShortcutMultiMappingInfo(info));
+                    }
+                } else {
+                    List<ShortcutMultiMappingInfo> shortcuts = new ArrayList<>();
+                    shortcuts.add(new ShortcutMultiMappingInfo(info));
+                    shortcutMap.put(label, shortcuts);
+                }
+            }
+            for (List<ShortcutMultiMappingInfo> shortcutInfos : shortcutMap.values()) {
+                for (ShortcutMultiMappingInfo shortcutInfo : shortcutInfos) {
+                    mappedGroup.addItem(shortcutInfo);
+                }
+            }
+            keyboardShortcutMultiMappingGroups.add(mappedGroup);
         }
         return keyboardShortcutMultiMappingGroups;
     }
@@ -1377,7 +1407,9 @@
         ShortcutMultiMappingInfo(KeyboardShortcutInfo info) {
             mLabel = info.getLabel();
             mIcon = info.getIcon();
-            mShortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup(info, null));
+            mShortcutKeyGroups = new ArrayList<>(
+                Arrays.asList(new ShortcutKeyGroup(info, null))
+            );
         }
 
         CharSequence getLabel() {
@@ -1388,6 +1420,10 @@
             return mIcon;
         }
 
+        void addShortcutKeyGroup(ShortcutKeyGroup group) {
+            mShortcutKeyGroups.add(group);
+        }
+
         List<ShortcutKeyGroup> getShortcutKeyGroups() {
             return mShortcutKeyGroups;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 4ee8349..81f644f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -352,10 +352,6 @@
     /** Called by the touch helper when the drag down was aborted and should be reset. */
     internal fun onDragDownReset() {
         logger.logDragDownAborted()
-        nsslController.setDimmed(
-            /* dimmed= */ true,
-            /* animate= */ true,
-        )
         nsslController.resetScrollPosition()
         nsslController.resetCheckSnoozeLeavebehind()
         setDragDownAmountAnimated(0f)
@@ -366,12 +362,7 @@
      *
      * @param above whether they dragged above it
      */
-    internal fun onCrossedThreshold(above: Boolean) {
-        nsslController.setDimmed(
-            /* dimmed= */ !above,
-            /* animate= */ true,
-        )
-    }
+    internal fun onCrossedThreshold(above: Boolean) {}
 
     /** Called by the touch helper when the drag down was started */
     internal fun onDragDownStarted(startingChild: ExpandableView?) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index 642eacc..c4d9cbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -35,6 +35,10 @@
 import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
 import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel
+import com.android.systemui.qs.tiles.impl.internet.domain.InternetTileMapper
+import com.android.systemui.qs.tiles.impl.internet.domain.interactor.InternetTileDataInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.interactor.InternetTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
 import com.android.systemui.qs.tiles.impl.saver.domain.DataSaverTileMapper
 import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileDataInteractor
 import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileUserActionInteractor
@@ -90,6 +94,7 @@
 
         const val AIRPLANE_MODE_TILE_SPEC = "airplane"
         const val DATA_SAVER_TILE_SPEC = "saver"
+        const val INTERNET_TILE_SPEC = "internet"
 
         /** Inject InternetTile or InternetTileNewImpl into tileMap in QSModule */
         @Provides
@@ -168,5 +173,36 @@
                 stateInteractor,
                 mapper,
             )
+
+        @Provides
+        @IntoMap
+        @StringKey(INTERNET_TILE_SPEC)
+        fun provideInternetTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(INTERNET_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.ic_qs_no_internet_available,
+                        labelRes = R.string.quick_settings_internet_label,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /** Inject InternetTile into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(INTERNET_TILE_SPEC)
+        fun provideInternetTileViewModel(
+            factory: QSTileViewModelFactory.Static<InternetTileModel>,
+            mapper: InternetTileMapper,
+            stateInteractor: InternetTileDataInteractor,
+            userActionInteractor: InternetTileUserActionInteractor
+        ): QSTileViewModel =
+            factory.create(
+                TileSpec.create(INTERNET_TILE_SPEC),
+                userActionInteractor,
+                stateInteractor,
+                mapper,
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt
new file mode 100644
index 0000000..1bebbfd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt
@@ -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.systemui.statusbar.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/**
+ * Whether to set the status bar keyguard view occluded or not, and whether to animate that change.
+ */
+data class OccludedState(
+    val occluded: Boolean,
+    val animate: Boolean = false,
+)
+
+/** Handles logic around calls to [StatusBarKeyguardViewManager] in legacy code. */
+@Deprecated("Will be removed once all of SBKVM's responsibilies are refactored.")
+@SysUISingleton
+class StatusBarKeyguardViewManagerInteractor
+@Inject
+constructor(
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    powerInteractor: PowerInteractor,
+) {
+    /** Occlusion state to apply whenever a keyguard transition is STARTED, if any. */
+    private val occlusionStateFromStartedStep: Flow<OccludedState> =
+        keyguardTransitionInteractor.startedKeyguardTransitionStep
+            .sample(powerInteractor.detailedWakefulness, ::Pair)
+            .map { (startedStep, wakefulness) ->
+                val transitioningFromPowerButtonGesture =
+                    KeyguardState.deviceIsAsleepInState(startedStep.from) &&
+                        startedStep.to == KeyguardState.OCCLUDED &&
+                        wakefulness.powerButtonLaunchGestureTriggered
+
+                if (
+                    startedStep.to == KeyguardState.OCCLUDED && !transitioningFromPowerButtonGesture
+                ) {
+                    // Set occluded upon STARTED, *unless* we're transitioning from the power
+                    // button, in which case we're going to play an animation over the lockscreen UI
+                    // and need to remain unoccluded until the transition finishes.
+                    return@map OccludedState(occluded = true, animate = false)
+                }
+
+                if (
+                    startedStep.from == KeyguardState.OCCLUDED &&
+                        startedStep.to == KeyguardState.LOCKSCREEN
+                ) {
+                    // Set unoccluded immediately ONLY if we're transitioning back to the lockscreen
+                    // since we need the views visible to animate them back down. This is a special
+                    // case due to the way unocclusion remote animations are run. We can remove this
+                    // once the unocclude animation uses the return animation framework.
+                    return@map OccludedState(occluded = false, animate = false)
+                }
+
+                // Otherwise, wait for the transition to FINISH to decide.
+                return@map null
+            }
+            .filterNotNull()
+
+    /** Occlusion state to apply whenever a keyguard transition is FINISHED. */
+    private val occlusionStateFromFinishedStep =
+        keyguardTransitionInteractor.finishedKeyguardTransitionStep
+            .sample(keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop, ::Pair)
+            .map { (finishedStep, showWhenLockedOnTop) ->
+                // If we're FINISHED in OCCLUDED, we want to render as occluded. We also need to
+                // remain occluded if a SHOW_WHEN_LOCKED activity is on the top of the task stack,
+                // and we're in any state other than GONE. This is necessary, for example, when we
+                // transition from OCCLUDED to a bouncer state. Otherwise, we should not be
+                // occluded.
+                val occluded =
+                    finishedStep.to == KeyguardState.OCCLUDED ||
+                        (showWhenLockedOnTop && finishedStep.to != KeyguardState.GONE)
+                OccludedState(occluded = occluded, animate = false)
+            }
+
+    /** Occlusion state to apply to SKBVM's setOccluded call. */
+    val keyguardViewOcclusionState =
+        merge(occlusionStateFromStartedStep, occlusionStateFromFinishedStep)
+            .distinctUntilChangedBy {
+                // Don't switch 'animate' values mid-transition.
+                it.occluded
+            }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index ea9df9a..05e8717 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -275,15 +275,6 @@
         return getHeight();
     }
 
-    /**
-     * Sets the notification as dimmed. The default implementation does nothing.
-     *
-     * @param dimmed Whether the notification should be dimmed.
-     * @param fade Whether an animation should be played to change the state.
-     */
-    public void setDimmed(boolean dimmed, boolean fade) {
-    }
-
     public boolean isRemoved() {
         return false;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
index dab89c5..b90aa10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
@@ -19,7 +19,9 @@
 import android.widget.flags.Flags.notifLinearlayoutOptimized
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing
 import javax.inject.Inject
+import javax.inject.Provider
 
 interface NotifRemoteViewsFactoryContainer {
     val factories: Set<NotifRemoteViewsFactory>
@@ -31,7 +33,8 @@
     featureFlags: FeatureFlags,
     precomputedTextViewFactory: PrecomputedTextViewFactory,
     bigPictureLayoutInflaterFactory: BigPictureLayoutInflaterFactory,
-    optimizedLinearLayoutFactory: NotificationOptimizedLinearLayoutFactory
+    optimizedLinearLayoutFactory: NotificationOptimizedLinearLayoutFactory,
+    notificationViewFlipperFactory: Provider<NotificationViewFlipperFactory>,
 ) : NotifRemoteViewsFactoryContainer {
     override val factories: Set<NotifRemoteViewsFactory> = buildSet {
         add(precomputedTextViewFactory)
@@ -41,5 +44,8 @@
         if (notifLinearlayoutOptimized()) {
             add(optimizedLinearLayoutFactory)
         }
+        if (NotificationViewFlipperPausing.isEnabled) {
+            add(notificationViewFlipperFactory.get())
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index c17ee39..f835cca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -42,6 +42,7 @@
 import android.view.ViewGroup;
 import android.widget.RemoteViews;
 
+import com.android.app.tracing.TraceUtils;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.widget.ImageMessageConsumer;
 import com.android.systemui.dagger.SysUISingleton;
@@ -369,49 +370,55 @@
             ExpandableNotificationRow row,
             NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
             NotificationContentInflaterLogger logger) {
-        InflationProgress result = new InflationProgress();
-        final NotificationEntry entryForLogging = row.getEntry();
+        return TraceUtils.trace("NotificationContentInflater.createRemoteViews", () -> {
+            InflationProgress result = new InflationProgress();
+            final NotificationEntry entryForLogging = row.getEntry();
 
-        if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
-            logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
-            result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
-        }
-
-        if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
-            logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
-            result.newExpandedView = createExpandedView(builder, isLowPriority);
-        }
-
-        if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
-            logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view");
-            result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
-        }
-
-        if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
-            logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
-            result.newPublicView = builder.makePublicContentView(isLowPriority);
-        }
-
-        if (AsyncGroupHeaderViewInflation.isEnabled()) {
-            if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
-                logger.logAsyncTaskProgress(entryForLogging,
-                        "creating group summary remote view");
-                result.mNewGroupHeaderView = builder.makeNotificationGroupHeader();
+            if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
+                logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
+                result.newContentView = createContentView(builder, isLowPriority,
+                        usesIncreasedHeight);
             }
 
-            if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
-                logger.logAsyncTaskProgress(entryForLogging,
-                        "creating low-priority group summary remote view");
-                result.mNewLowPriorityGroupHeaderView =
-                        builder.makeLowPriorityContentView(true /* useRegularSubtext */);
+            if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
+                logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
+                result.newExpandedView = createExpandedView(builder, isLowPriority);
             }
-        }
-        setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider);
-        result.packageContext = packageContext;
-        result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */);
-        result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
-                true /* showingPublic */);
-        return result;
+
+            if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
+                logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view");
+                result.newHeadsUpView = builder.createHeadsUpContentView(
+                        usesIncreasedHeadsUpHeight);
+            }
+
+            if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
+                logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
+                result.newPublicView = builder.makePublicContentView(isLowPriority);
+            }
+
+            if (AsyncGroupHeaderViewInflation.isEnabled()) {
+                if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
+                    logger.logAsyncTaskProgress(entryForLogging,
+                            "creating group summary remote view");
+                    result.mNewGroupHeaderView = builder.makeNotificationGroupHeader();
+                }
+
+                if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
+                    logger.logAsyncTaskProgress(entryForLogging,
+                            "creating low-priority group summary remote view");
+                    result.mNewLowPriorityGroupHeaderView =
+                            builder.makeLowPriorityContentView(true /* useRegularSubtext */);
+                }
+            }
+            setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider);
+            result.packageContext = packageContext;
+            result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(
+                    false /* showingPublic */);
+            result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
+                    true /* showingPublic */);
+
+            return result;
+        });
     }
 
     private static void setNotifsViewsInflaterFactory(InflationProgress result,
@@ -445,6 +452,8 @@
             RemoteViews.InteractionHandler remoteViewClickHandler,
             @Nullable InflationCallback callback,
             NotificationContentInflaterLogger logger) {
+        Trace.beginAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
+
         NotificationContentView privateLayout = row.getPrivateLayout();
         NotificationContentView publicLayout = row.getPublicLayout();
         final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
@@ -621,6 +630,7 @@
         cancellationSignal.setOnCancelListener(
                 () -> {
                     logger.logAsyncTaskProgress(entry, "apply cancelled");
+                    Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
                     runningInflations.values().forEach(CancellationSignal::cancel);
                 });
 
@@ -769,17 +779,17 @@
         if (!requiresHeightCheck(entry)) {
             return true;
         }
-        Trace.beginSection("NotificationContentInflater#satisfiesMinHeightRequirement");
-        int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
-        int referenceWidth = resources.getDimensionPixelSize(
-                R.dimen.notification_validation_reference_width);
-        int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, View.MeasureSpec.EXACTLY);
-        view.measure(widthSpec, heightSpec);
-        int minHeight = resources.getDimensionPixelSize(
-                R.dimen.notification_validation_minimum_allowed_height);
-        boolean result = view.getMeasuredHeight() >= minHeight;
-        Trace.endSection();
-        return result;
+        return TraceUtils.trace("NotificationContentInflater#satisfiesMinHeightRequirement", () -> {
+            int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+            int referenceWidth = resources.getDimensionPixelSize(
+                    R.dimen.notification_validation_reference_width);
+            int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth,
+                    View.MeasureSpec.EXACTLY);
+            view.measure(widthSpec, heightSpec);
+            int minHeight = resources.getDimensionPixelSize(
+                    R.dimen.notification_validation_minimum_allowed_height);
+            return view.getMeasuredHeight() >= minHeight;
+        });
     }
 
     /**
@@ -966,6 +976,7 @@
 
         entry.headsUpStatusBarText = result.headsUpStatusBarText;
         entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic;
+        Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
         if (endListener != null) {
             endListener.onAsyncInflationFinished(entry);
         }
@@ -1102,83 +1113,97 @@
         }
 
         @Override
+        protected void onPreExecute() {
+            Trace.beginAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
+        }
+
+        @Override
         protected InflationProgress doInBackground(Void... params) {
-            try {
-                final StatusBarNotification sbn = mEntry.getSbn();
-                // Ensure the ApplicationInfo is updated before a builder is recovered.
-                updateApplicationInfo(sbn);
-                final Notification.Builder recoveredBuilder
-                        = Notification.Builder.recoverBuilder(mContext,
-                        sbn.getNotification());
+            return TraceUtils.trace("NotificationContentInflater.AsyncInflationTask#doInBackground",
+                    () -> {
+                        try {
+                            return doInBackgroundInternal();
+                        } catch (Exception e) {
+                            mError = e;
+                            mLogger.logAsyncTaskException(mEntry, "inflating", e);
+                            return null;
+                        }
+                    });
+        }
 
-                Context packageContext = sbn.getPackageContext(mContext);
-                if (recoveredBuilder.usesTemplate()) {
-                    // For all of our templates, we want it to be RTL
-                    packageContext = new RtlEnabledContext(packageContext);
-                }
-                boolean isConversation = mEntry.getRanking().isConversation();
-                Notification.MessagingStyle messagingStyle = null;
-                if (isConversation) {
-                    messagingStyle = mConversationProcessor.processNotification(
-                            mEntry, recoveredBuilder, mLogger);
-                }
-                InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
-                        recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
-                        mUsesIncreasedHeadsUpHeight, packageContext, mRow,
-                        mNotifLayoutInflaterFactoryProvider, mLogger);
+        private InflationProgress doInBackgroundInternal() {
+            final StatusBarNotification sbn = mEntry.getSbn();
+            // Ensure the ApplicationInfo is updated before a builder is recovered.
+            updateApplicationInfo(sbn);
+            final Notification.Builder recoveredBuilder = Notification.Builder.recoverBuilder(
+                    mContext, sbn.getNotification());
 
-                mLogger.logAsyncTaskProgress(mEntry,
-                        "getting existing smart reply state (on wrong thread!)");
-                InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState();
-                mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
-                InflationProgress result = inflateSmartReplyViews(
-                        /* result = */ inflationProgress,
-                        mReInflateFlags,
-                        mEntry,
-                        mContext,
-                        packageContext,
-                        previousSmartReplyState,
-                        mSmartRepliesInflater,
-                        mLogger);
-
-                if (AsyncHybridViewInflation.isEnabled()) {
-                    // Inflate the single-line content view's ViewModel and ViewHolder from the
-                    // background thread, the ViewHolder needs to be bind with ViewModel later from
-                    // the main thread.
-                    result.mInflatedSingleLineViewModel = SingleLineViewInflater
-                            .inflateSingleLineViewModel(
-                                    mEntry.getSbn().getNotification(),
-                                    messagingStyle,
-                                    recoveredBuilder,
-                                    mContext
-                            );
-                    result.mInflatedSingleLineViewHolder =
-                            SingleLineViewInflater.inflateSingleLineViewHolder(
-                                    isConversation,
-                                    mReInflateFlags,
-                                    mEntry,
-                                    mContext,
-                                    mLogger
-                            );
-                }
-
-                mLogger.logAsyncTaskProgress(mEntry,
-                        "getting row image resolver (on wrong thread!)");
-                final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
-                // wait for image resolver to finish preloading
-                mLogger.logAsyncTaskProgress(mEntry, "waiting for preloaded images");
-                imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
-
-                return result;
-            } catch (Exception e) {
-                mError = e;
-                mLogger.logAsyncTaskException(mEntry, "inflating", e);
-                return null;
+            Context packageContext = sbn.getPackageContext(mContext);
+            if (recoveredBuilder.usesTemplate()) {
+                // For all of our templates, we want it to be RTL
+                packageContext = new RtlEnabledContext(packageContext);
             }
+            boolean isConversation = mEntry.getRanking().isConversation();
+            Notification.MessagingStyle messagingStyle = null;
+            if (isConversation) {
+                messagingStyle = mConversationProcessor.processNotification(
+                        mEntry, recoveredBuilder, mLogger);
+            }
+            InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
+                    recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
+                    mUsesIncreasedHeadsUpHeight, packageContext, mRow,
+                    mNotifLayoutInflaterFactoryProvider, mLogger);
+
+            mLogger.logAsyncTaskProgress(mEntry,
+                    "getting existing smart reply state (on wrong thread!)");
+            InflatedSmartReplyState previousSmartReplyState =
+                    mRow.getExistingSmartReplyState();
+            mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
+            InflationProgress result = inflateSmartReplyViews(
+                    /* result = */ inflationProgress,
+                    mReInflateFlags,
+                    mEntry,
+                    mContext,
+                    packageContext,
+                    previousSmartReplyState,
+                    mSmartRepliesInflater,
+                    mLogger);
+
+            if (AsyncHybridViewInflation.isEnabled()) {
+                // Inflate the single-line content view's ViewModel and ViewHolder from the
+                // background thread, the ViewHolder needs to be bind with ViewModel later from
+                // the main thread.
+                result.mInflatedSingleLineViewModel = SingleLineViewInflater
+                        .inflateSingleLineViewModel(
+                                mEntry.getSbn().getNotification(),
+                                messagingStyle,
+                                recoveredBuilder,
+                                mContext
+                        );
+                result.mInflatedSingleLineViewHolder =
+                        SingleLineViewInflater.inflateSingleLineViewHolder(
+                                isConversation,
+                                mReInflateFlags,
+                                mEntry,
+                                mContext,
+                                mLogger
+                        );
+            }
+
+            mLogger.logAsyncTaskProgress(mEntry,
+                    "getting row image resolver (on wrong thread!)");
+            final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
+            // wait for image resolver to finish preloading
+            mLogger.logAsyncTaskProgress(mEntry, "waiting for preloaded images");
+            imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
+
+            return result;
         }
 
         @Override
         protected void onPostExecute(InflationProgress result) {
+            Trace.endAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
+
             if (mError == null) {
                 // Logged in detail in apply.
                 mCancellationSignal = apply(
@@ -1197,6 +1222,11 @@
             }
         }
 
+        @Override
+        protected void onCancelled(InflationProgress result) {
+            Trace.endAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
+        }
+
         private void handleError(Exception e) {
             mEntry.onInflationTaskFinished();
             StatusBarNotification sbn = mEntry.getSbn();
@@ -1294,4 +1324,8 @@
         public abstract void setResultView(View v);
         public abstract RemoteViews getRemoteView();
     }
+
+    private static final String ASYNC_TASK_TRACE_METHOD =
+            "NotificationContentInflater.AsyncInflationTask";
+    private static final String APPLY_TRACE_METHOD = "NotificationContentInflater#apply";
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt
new file mode 100644
index 0000000..0594c12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ViewFlipper
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.NotificationViewFlipperBinder
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.NotificationViewFlipperViewModel
+import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing
+import javax.inject.Inject
+
+/**
+ * A factory which owns the construction of any ViewFlipper inside of Notifications, and binds it
+ * with a view model. This ensures that ViewFlippers are paused when the keyguard is showing.
+ */
+class NotificationViewFlipperFactory
+@Inject
+constructor(
+    private val viewModel: NotificationViewFlipperViewModel,
+) : NotifRemoteViewsFactory {
+    init {
+        /* check if */ NotificationViewFlipperPausing.isUnexpectedlyInLegacyMode()
+    }
+
+    override fun instantiate(
+        row: ExpandableNotificationRow,
+        @InflationFlag layoutType: Int,
+        parent: View?,
+        name: String,
+        context: Context,
+        attrs: AttributeSet
+    ): View? {
+        return when (name) {
+            ViewFlipper::class.java.name,
+            ViewFlipper::class.java.simpleName ->
+                ViewFlipper(context, attrs).also { viewFlipper ->
+                    NotificationViewFlipperBinder.bindWhileAttached(viewFlipper, viewModel)
+                }
+            else -> null
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.kt
new file mode 100644
index 0000000..133d3e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.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.statusbar.notification.row.ui.viewbinder
+
+import android.widget.ViewFlipper
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.NotificationViewFlipperViewModel
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+/** Binds a [NotificationViewFlipper] to its [view model][NotificationViewFlipperViewModel]. */
+object NotificationViewFlipperBinder {
+    fun bindWhileAttached(
+        viewFlipper: ViewFlipper,
+        viewModel: NotificationViewFlipperViewModel,
+    ): DisposableHandle {
+        if (viewFlipper.isAutoStart) {
+            // If the ViewFlipper is not set to AutoStart, the pause binding is meaningless
+            return DisposableHandle {}
+        }
+        return viewFlipper.repeatWhenAttached {
+            lifecycleScope.launch { bind(viewFlipper, viewModel) }
+        }
+    }
+
+    suspend fun bind(
+        viewFlipper: ViewFlipper,
+        viewModel: NotificationViewFlipperViewModel,
+    ) = coroutineScope { launch { viewModel.isPaused.collect { viewFlipper.setPaused(it) } } }
+
+    private fun ViewFlipper.setPaused(paused: Boolean) {
+        if (paused) {
+            stopFlipping()
+        } else if (isAutoStart) {
+            startFlipping()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.kt
new file mode 100644
index 0000000..7694e58
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.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.statusbar.notification.row.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing
+import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import javax.inject.Inject
+
+/** A model which represents whether ViewFlippers inside notifications should be paused. */
+@SysUISingleton
+class NotificationViewFlipperViewModel
+@Inject
+constructor(
+    dumpManager: DumpManager,
+    stackInteractor: NotificationStackInteractor,
+) : FlowDumperImpl(dumpManager) {
+    init {
+        /* check if */ NotificationViewFlipperPausing.isUnexpectedlyInLegacyMode()
+    }
+
+    val isPaused = stackInteractor.isShowingOnLockscreen.dumpWhileCollecting("isPaused")
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt
new file mode 100644
index 0000000..cea6a2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notification view flipper pausing flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationViewFlipperPausing {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationViewFlipperPausing()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index c90acee..ab2f664 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -61,7 +61,6 @@
      */
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private int mScrollY;
-    private boolean mDimmed;
     private float mOverScrollTopAmount;
     private float mOverScrollBottomAmount;
     private boolean mDozing;
@@ -344,14 +343,6 @@
         this.mScrollY = Math.max(scrollY, 0);
     }
 
-    /**
-     * @param dimmed Whether we are in a dimmed state (on the lockscreen), where the backgrounds are
-     *               translucent and everything is scaled back a bit.
-     */
-    public void setDimmed(boolean dimmed) {
-        mDimmed = dimmed;
-    }
-
     /** While dozing, we draw as little as possible, assuming a black background */
     public void setDozing(boolean dozing) {
         mDozing = dozing;
@@ -375,12 +366,6 @@
         mHideSensitive = hideSensitive;
     }
 
-    public boolean isDimmed() {
-        // While we are expanding from pulse, we want the notifications not to be dimmed, otherwise
-        // you'd see the difference to the pulsing notification
-        return mDimmed && !(isPulseExpanding() && mDozeAmount == 1.0f);
-    }
-
     public boolean isDozing() {
         return mDozing;
     }
@@ -768,7 +753,6 @@
         pw.println("mHideSensitive=" + mHideSensitive);
         pw.println("mShadeExpanded=" + mShadeExpanded);
         pw.println("mClearAllInProgress=" + mClearAllInProgress);
-        pw.println("mDimmed=" + mDimmed);
         pw.println("mStatusBarState=" + mStatusBarState);
         pw.println("mExpansionChanging=" + mExpansionChanging);
         pw.println("mPanelFullWidth=" + mIsSmallScreen);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
index 5343cbf..03a1082 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java
@@ -35,7 +35,6 @@
     boolean animateZ;
     boolean animateHeight;
     boolean animateTopInset;
-    boolean animateDimmed;
     boolean animateHideSensitive;
     boolean hasDelays;
     boolean hasGoToFullShadeEvent;
@@ -83,11 +82,6 @@
         return this;
     }
 
-    public AnimationFilter animateDimmed() {
-        animateDimmed = true;
-        return this;
-    }
-
     public AnimationFilter animateHideSensitive() {
         animateHideSensitive = true;
         return this;
@@ -128,7 +122,6 @@
         animateZ |= filter.animateZ;
         animateHeight |= filter.animateHeight;
         animateTopInset |= filter.animateTopInset;
-        animateDimmed |= filter.animateDimmed;
         animateHideSensitive |= filter.animateHideSensitive;
         hasDelays |= filter.hasDelays;
         mAnimatedProperties.addAll(filter.mAnimatedProperties);
@@ -142,7 +135,6 @@
         animateZ = false;
         animateHeight = false;
         animateTopInset = false;
-        animateDimmed = false;
         animateHideSensitive = false;
         hasDelays = false;
         hasGoToFullShadeEvent = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
index d0c5c82..d1e5ab0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
@@ -88,7 +88,6 @@
             | ExpandableViewState.LOCATION_MAIN_AREA;
 
     public int height;
-    public boolean dimmed;
     public boolean hideSensitive;
     public boolean belowSpeedBump;
     public boolean inShelf;
@@ -128,7 +127,6 @@
         if (viewState instanceof ExpandableViewState) {
             ExpandableViewState svs = (ExpandableViewState) viewState;
             height = svs.height;
-            dimmed = svs.dimmed;
             hideSensitive = svs.hideSensitive;
             belowSpeedBump = svs.belowSpeedBump;
             clipTopAmount = svs.clipTopAmount;
@@ -155,9 +153,6 @@
                 expandableView.setActualHeight(newHeight, false /* notifyListeners */);
             }
 
-            // apply dimming
-            expandableView.setDimmed(this.dimmed, false /* animate */);
-
             // apply hiding sensitive
             expandableView.setHideSensitive(
                     this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */);
@@ -216,9 +211,6 @@
             abortAnimation(child, TAG_ANIMATOR_BOTTOM_INSET);
         }
 
-        // start dimmed animation
-        expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed);
-
         // apply below the speed bump
         if (!NotificationIconContainerRefactor.isEnabled()) {
             expandableView.setBelowSpeedBump(this.belowSpeedBump);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index fa97300..28f874d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -795,7 +795,6 @@
             } else {
                 childState.setZTranslation(0);
             }
-            childState.dimmed = parentState.dimmed;
             childState.hideSensitive = parentState.hideSensitive;
             childState.belowSpeedBump = parentState.belowSpeedBump;
             childState.clipTopAmount = 0;
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 27db84f..bfda6d5 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
@@ -29,10 +29,6 @@
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.TimeAnimator;
-import android.animation.ValueAnimator;
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.FloatRange;
@@ -79,7 +75,6 @@
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.graphics.ColorUtils;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.keyguard.BouncerPanelExpansionCalculator;
@@ -163,18 +158,11 @@
      * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
      */
     private static final int INVALID_POINTER = -1;
-    /**
-     * The distance in pixels between sections when the sections are directly adjacent (no visible
-     * gap is drawn between them). In this case we don't want to round their corners.
-     */
-    private static final int DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX = 1;
     private boolean mKeyguardBypassEnabled;
 
     private final ExpandHelper mExpandHelper;
     private NotificationSwipeHelper mSwipeHelper;
     private int mCurrentStackHeight = Integer.MAX_VALUE;
-    private final Paint mBackgroundPaint = new Paint();
-    private final boolean mShouldDrawNotificationBackground;
     private boolean mHighPriorityBeforeSpeedBump;
 
     private float mExpandedHeight;
@@ -263,7 +251,6 @@
     private OnEmptySpaceClickListener mOnEmptySpaceClickListener;
     private boolean mNeedsAnimation;
     private boolean mTopPaddingNeedsAnimation;
-    private boolean mDimmedNeedsAnimation;
     private boolean mHideSensitiveNeedsAnimation;
     private boolean mActivateNeedsAnimation;
     private boolean mGoToFullShadeNeedsAnimation;
@@ -350,40 +337,15 @@
         }
     };
     private final NotificationSection[] mSections;
-    private boolean mAnimateNextBackgroundTop;
-    private boolean mAnimateNextBackgroundBottom;
-    private boolean mAnimateNextSectionBoundsChange;
-    private @ColorInt int mBgColor;
-    private float mDimAmount;
-    private ValueAnimator mDimAnimator;
     private final ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
-    private final Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            mDimAnimator = null;
-        }
-    };
-    private final ValueAnimator.AnimatorUpdateListener mDimUpdateListener
-            = new ValueAnimator.AnimatorUpdateListener() {
-
-        @Override
-        public void onAnimationUpdate(ValueAnimator animation) {
-            setDimAmount((Float) animation.getAnimatedValue());
-        }
-    };
     protected ViewGroup mQsHeader;
     // Rect of QsHeader. Kept as a field just to avoid creating a new one each time.
     private final Rect mQsHeaderBound = new Rect();
     private boolean mContinuousShadowUpdate;
-    private boolean mContinuousBackgroundUpdate;
     private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> {
         updateViewShadows();
         return true;
     };
-    private final ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> {
-        updateBackground();
-        return true;
-    };
     private final Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> {
         float endY = view.getTranslationY() + view.getActualHeight();
         float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
@@ -481,7 +443,6 @@
     private boolean mHeadsUpAnimatingAway;
     private int mStatusBarState;
     private int mUpcomingStatusBarState;
-    private int mCachedBackgroundColor;
     private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
     private final Runnable mReflingAndAnimateScroll = this::animateScroll;
     private int mCornerRadius;
@@ -581,7 +542,6 @@
      */
     private boolean mDismissUsingRowTranslationX = true;
     private ExpandableNotificationRow mTopHeadsUpRow;
-    private long mNumHeadsUp;
     private NotificationStackScrollLayoutController.TouchHandler mTouchHandler;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private boolean mShouldUseSplitNotificationShade;
@@ -595,7 +555,7 @@
         mSplitShadeStateController = splitShadeStateController;
         updateSplitNotificationShade();
     }
-    private FeatureFlags mFeatureFlags;
+    private final FeatureFlags mFeatureFlags;
 
     private final ExpandableView.OnHeightChangedListener mOnChildHeightChangedListener =
             new ExpandableView.OnHeightChangedListener() {
@@ -667,8 +627,6 @@
         mSections = mSectionsManager.createSectionsForBuckets();
 
         mAmbientState = Dependency.get(AmbientState.class);
-        mBgColor = Utils.getColorAttr(mContext,
-                com.android.internal.R.attr.materialColorSurfaceContainerHigh).getDefaultColor();
         int minHeight = res.getDimensionPixelSize(R.dimen.notification_min_height);
         int maxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height);
         mSplitShadeMinContentHeight = res.getDimensionPixelSize(
@@ -680,16 +638,12 @@
 
         mStackScrollAlgorithm = createStackScrollAlgorithm(context);
         mStateAnimator = new StackStateAnimator(context, this);
-        mShouldDrawNotificationBackground =
-                res.getBoolean(R.bool.config_drawNotificationBackground);
         setOutlineProvider(mOutlineProvider);
 
         // We could set this whenever we 'requestChildUpdate' much like the viewTreeObserver, but
         // that adds a bunch of complexity, and drawing nothing isn't *that* expensive.
-        boolean willDraw = SceneContainerFlag.isEnabled()
-                || mShouldDrawNotificationBackground || mDebugLines;
+        boolean willDraw = SceneContainerFlag.isEnabled() || mDebugLines;
         setWillNotDraw(!willDraw);
-        mBackgroundPaint.setAntiAlias(true);
         if (mDebugLines) {
             mDebugPaint = new Paint();
             mDebugPaint.setColor(0xffff0000);
@@ -812,9 +766,6 @@
     }
 
     void updateBgColor() {
-        mBgColor = Utils.getColorAttr(mContext,
-                com.android.internal.R.attr.materialColorSurfaceContainerHigh).getDefaultColor();
-        updateBackgroundDimming();
         for (int i = 0; i < getChildCount(); i++) {
             View child = getChildAt(i);
             if (child instanceof ActivatableNotificationView activatableView) {
@@ -835,14 +786,6 @@
 
     protected void onDraw(Canvas canvas) {
         onJustBeforeDraw();
-        if (mShouldDrawNotificationBackground
-                && (mSections[0].getCurrentBounds().top
-                < mSections[mSections.length - 1].getCurrentBounds().bottom
-                || mAmbientState.isDozing())) {
-            drawBackground(canvas);
-        } else if (mInHeadsUpPinnedMode || mHeadsUpAnimatingAway) {
-            drawHeadsUpBackground(canvas);
-        }
 
         if (mDebugLines) {
             onDrawDebug(canvas);
@@ -930,150 +873,6 @@
         return textY;
     }
 
-    private void drawBackground(Canvas canvas) {
-        int lockScreenLeft = mSidePaddings;
-        int lockScreenRight = getWidth() - mSidePaddings;
-        int lockScreenTop = mSections[0].getCurrentBounds().top;
-        int lockScreenBottom = mSections[mSections.length - 1].getCurrentBounds().bottom;
-        int hiddenLeft = getWidth() / 2;
-        int hiddenTop = mTopPadding;
-
-        float yProgress = 1 - mInterpolatedHideAmount;
-        float xProgress = mHideXInterpolator.getInterpolation(
-                (1 - mLinearHideAmount) * mBackgroundXFactor);
-
-        int left = (int) MathUtils.lerp(hiddenLeft, lockScreenLeft, xProgress);
-        int right = (int) MathUtils.lerp(hiddenLeft, lockScreenRight, xProgress);
-        int top = (int) MathUtils.lerp(hiddenTop, lockScreenTop, yProgress);
-        int bottom = (int) MathUtils.lerp(hiddenTop, lockScreenBottom, yProgress);
-        mBackgroundAnimationRect.set(
-                left,
-                top,
-                right,
-                bottom);
-
-        int backgroundTopAnimationOffset = top - lockScreenTop;
-        // TODO(kprevas): this may not be necessary any more since we don't display the shelf in AOD
-        boolean anySectionHasVisibleChild = false;
-        for (NotificationSection section : mSections) {
-            if (section.needsBackground()) {
-                anySectionHasVisibleChild = true;
-                break;
-            }
-        }
-        boolean shouldDrawBackground;
-        if (mKeyguardBypassEnabled && onKeyguard()) {
-            shouldDrawBackground = isPulseExpanding();
-        } else {
-            shouldDrawBackground = !mAmbientState.isDozing() || anySectionHasVisibleChild;
-        }
-        if (shouldDrawBackground) {
-            drawBackgroundRects(canvas, left, right, top, backgroundTopAnimationOffset);
-        }
-
-        updateClipping();
-    }
-
-    /**
-     * Draws round rects for each background section.
-     * <p>
-     * We want to draw a round rect for each background section as defined by {@link #mSections}.
-     * However, if two sections are directly adjacent with no gap between them (e.g. on the
-     * lockscreen where the shelf can appear directly below the high priority section, or while
-     * scrolling the shade so that the top of the shelf is right at the bottom of the high priority
-     * section), we don't want to round the adjacent corners.
-     * <p>
-     * Since {@link Canvas} doesn't provide a way to draw a half-rounded rect, this means that we
-     * need to coalesce the backgrounds for adjacent sections and draw them as a single round rect.
-     * This method tracks the top of each rect we need to draw, then iterates through the visible
-     * sections.  If a section is not adjacent to the previous section, we draw the previous rect
-     * behind the sections we've accumulated up to that point, then start a new rect at the top of
-     * the current section.  When we're done iterating we will always have one rect left to draw.
-     */
-    private void drawBackgroundRects(Canvas canvas, int left, int right, int top,
-                                     int animationYOffset) {
-        int backgroundRectTop = top;
-        int lastSectionBottom =
-                mSections[0].getCurrentBounds().bottom + animationYOffset;
-        int currentLeft = left;
-        int currentRight = right;
-        boolean first = true;
-        for (NotificationSection section : mSections) {
-            if (!section.needsBackground()) {
-                continue;
-            }
-            int sectionTop = section.getCurrentBounds().top + animationYOffset;
-            int ownLeft = Math.min(Math.max(left, section.getCurrentBounds().left), right);
-            int ownRight = Math.max(Math.min(right, section.getCurrentBounds().right), ownLeft);
-            // If sections are directly adjacent to each other, we don't want to draw them
-            // as separate roundrects, as the rounded corners right next to each other look
-            // bad.
-            if (sectionTop - lastSectionBottom > DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX
-                    || ((currentLeft != ownLeft || currentRight != ownRight) && !first)) {
-                canvas.drawRoundRect(currentLeft,
-                        backgroundRectTop,
-                        currentRight,
-                        lastSectionBottom,
-                        mCornerRadius, mCornerRadius, mBackgroundPaint);
-                backgroundRectTop = sectionTop;
-            }
-            currentLeft = ownLeft;
-            currentRight = ownRight;
-            lastSectionBottom =
-                    section.getCurrentBounds().bottom + animationYOffset;
-            first = false;
-        }
-        canvas.drawRoundRect(currentLeft,
-                backgroundRectTop,
-                currentRight,
-                lastSectionBottom,
-                mCornerRadius, mCornerRadius, mBackgroundPaint);
-    }
-
-    private void drawHeadsUpBackground(Canvas canvas) {
-        int left = mSidePaddings;
-        int right = getWidth() - mSidePaddings;
-
-        float top = getHeight();
-        float bottom = 0;
-        int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            if (child.getVisibility() != View.GONE
-                    && child instanceof ExpandableNotificationRow row) {
-                if ((row.isPinned() || row.isHeadsUpAnimatingAway()) && row.getTranslation() < 0
-                        && row.getProvider().shouldShowGutsOnSnapOpen()) {
-                    top = Math.min(top, row.getTranslationY());
-                    bottom = Math.max(bottom, row.getTranslationY() + row.getActualHeight());
-                }
-            }
-        }
-
-        if (top < bottom) {
-            canvas.drawRoundRect(
-                    left, top, right, bottom,
-                    mCornerRadius, mCornerRadius, mBackgroundPaint);
-        }
-    }
-
-    void updateBackgroundDimming() {
-        // No need to update the background color if it's not being drawn.
-        if (!mShouldDrawNotificationBackground) {
-            return;
-        }
-        // Interpolate between semi-transparent notification panel background color
-        // and white AOD separator.
-        float colorInterpolation = MathUtils.smoothStep(0.4f /* start */, 1f /* end */,
-                mLinearHideAmount);
-        int color = ColorUtils.blendARGB(mBgColor, Color.WHITE, colorInterpolation);
-
-        if (mCachedBackgroundColor != color) {
-            mCachedBackgroundColor = color;
-            mBackgroundPaint.setColor(color);
-            invalidate();
-        }
-    }
-
     private void reinitView() {
         initView(getContext(), mSwipeHelper, mNotificationStackSizeCalculator);
     }
@@ -1359,9 +1158,6 @@
 
     private void onPreDrawDuringAnimation() {
         mShelf.updateAppearance();
-        if (!mNeedsAnimation && !mChildrenUpdateRequested) {
-            updateBackground();
-        }
     }
 
     private void updateScrollStateForAddedChildren() {
@@ -2565,125 +2361,6 @@
         }
     }
 
-    private void updateBackground() {
-        // No need to update the background color if it's not being drawn.
-        if (!mShouldDrawNotificationBackground) {
-            return;
-        }
-
-        updateBackgroundBounds();
-        if (didSectionBoundsChange()) {
-            boolean animate = mAnimateNextSectionBoundsChange || mAnimateNextBackgroundTop
-                    || mAnimateNextBackgroundBottom || areSectionBoundsAnimating();
-            if (!isExpanded()) {
-                abortBackgroundAnimators();
-                animate = false;
-            }
-            if (animate) {
-                startBackgroundAnimation();
-            } else {
-                for (NotificationSection section : mSections) {
-                    section.resetCurrentBounds();
-                }
-                invalidate();
-            }
-        } else {
-            abortBackgroundAnimators();
-        }
-        mAnimateNextBackgroundTop = false;
-        mAnimateNextBackgroundBottom = false;
-        mAnimateNextSectionBoundsChange = false;
-    }
-
-    private void abortBackgroundAnimators() {
-        for (NotificationSection section : mSections) {
-            section.cancelAnimators();
-        }
-    }
-
-    private boolean didSectionBoundsChange() {
-        for (NotificationSection section : mSections) {
-            if (section.didBoundsChange()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean areSectionBoundsAnimating() {
-        for (NotificationSection section : mSections) {
-            if (section.areBoundsAnimating()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void startBackgroundAnimation() {
-        // TODO(kprevas): do we still need separate fields for top/bottom?
-        // or can each section manage its own animation state?
-        NotificationSection firstVisibleSection = getFirstVisibleSection();
-        NotificationSection lastVisibleSection = getLastVisibleSection();
-        for (NotificationSection section : mSections) {
-            section.startBackgroundAnimation(
-                    section == firstVisibleSection
-                            ? mAnimateNextBackgroundTop
-                            : mAnimateNextSectionBoundsChange,
-                    section == lastVisibleSection
-                            ? mAnimateNextBackgroundBottom
-                            : mAnimateNextSectionBoundsChange);
-        }
-    }
-
-    /**
-     * Update the background bounds to the new desired bounds
-     */
-    private void updateBackgroundBounds() {
-        int left = mSidePaddings;
-        int right = getWidth() - mSidePaddings;
-        for (NotificationSection section : mSections) {
-            section.getBounds().left = left;
-            section.getBounds().right = right;
-        }
-
-        if (!mIsExpanded) {
-            for (NotificationSection section : mSections) {
-                section.getBounds().top = 0;
-                section.getBounds().bottom = 0;
-            }
-            return;
-        }
-        int minTopPosition;
-        NotificationSection lastSection = getLastVisibleSection();
-        boolean onKeyguard = mStatusBarState == StatusBarState.KEYGUARD;
-        if (!onKeyguard) {
-            minTopPosition = (int) (mTopPadding + mStackTranslation);
-        } else if (lastSection == null) {
-            minTopPosition = mTopPadding;
-        } else {
-            // The first sections could be empty while there could still be elements in later
-            // sections. The position of these first few sections is determined by the position of
-            // the first visible section.
-            NotificationSection firstVisibleSection = getFirstVisibleSection();
-            firstVisibleSection.updateBounds(0 /* minTopPosition*/, 0 /* minBottomPosition */,
-                    false /* shiftPulsingWithFirst */);
-            minTopPosition = firstVisibleSection.getBounds().top;
-        }
-        boolean shiftPulsingWithFirst = mNumHeadsUp <= 1
-                && (mAmbientState.isDozing() || (mKeyguardBypassEnabled && onKeyguard));
-        for (NotificationSection section : mSections) {
-            int minBottomPosition = minTopPosition;
-            if (section == lastSection) {
-                // We need to make sure the section goes all the way to the shelf
-                minBottomPosition = (int) (ViewState.getFinalTranslationY(mShelf)
-                        + mShelf.getIntrinsicHeight());
-            }
-            minTopPosition = section.updateBounds(minTopPosition, minBottomPosition,
-                    shiftPulsingWithFirst);
-            shiftPulsingWithFirst = false;
-        }
-    }
-
     private NotificationSection getFirstVisibleSection() {
         for (NotificationSection section : mSections) {
             if (section.getFirstVisibleChild() != null) {
@@ -3184,13 +2861,7 @@
                 mSections, getChildrenWithBackground());
 
         if (mAnimationsEnabled && mIsExpanded) {
-            mAnimateNextBackgroundTop = firstChild != previousFirstChild;
-            mAnimateNextBackgroundBottom = lastChild != previousLastChild || mAnimateBottomOnLayout;
-            mAnimateNextSectionBoundsChange = sectionViewsChanged;
         } else {
-            mAnimateNextBackgroundTop = false;
-            mAnimateNextBackgroundBottom = false;
-            mAnimateNextSectionBoundsChange = false;
         }
         mAmbientState.setLastVisibleBackgroundChild(lastChild);
         mAnimateBottomOnLayout = false;
@@ -3344,7 +3015,6 @@
             setAnimationRunning(true);
             mStateAnimator.startAnimationForEvents(mAnimationEvents, mGoToFullShadeDelay);
             mAnimationEvents.clear();
-            updateBackground();
             updateViewShadows();
         } else {
             applyCurrentState();
@@ -3359,7 +3029,6 @@
         generatePositionChangeEvents();
         generateTopPaddingEvent();
         generateActivateEvent();
-        generateDimmedEvent();
         generateHideSensitiveEvent();
         generateGoToFullShadeEvent();
         generateViewResizeEvent();
@@ -3577,14 +3246,6 @@
         mEverythingNeedsAnimation = false;
     }
 
-    private void generateDimmedEvent() {
-        if (mDimmedNeedsAnimation) {
-            mAnimationEvents.add(
-                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
-        }
-        mDimmedNeedsAnimation = false;
-    }
-
     private void generateHideSensitiveEvent() {
         if (mHideSensitiveNeedsAnimation) {
             mAnimationEvents.add(
@@ -4486,48 +4147,6 @@
         mAnimationFinishedRunnables.clear();
     }
 
-    /**
-     * See {@link AmbientState#setDimmed}.
-     */
-    void setDimmed(boolean dimmed, boolean animate) {
-        dimmed &= onKeyguard();
-        mAmbientState.setDimmed(dimmed);
-        if (animate && mAnimationsEnabled) {
-            mDimmedNeedsAnimation = true;
-            mNeedsAnimation = true;
-            animateDimmed(dimmed);
-        } else {
-            setDimAmount(dimmed ? 1.0f : 0.0f);
-        }
-        requestChildrenUpdate();
-    }
-
-    @VisibleForTesting
-    boolean isDimmed() {
-        return mAmbientState.isDimmed();
-    }
-
-    private void setDimAmount(float dimAmount) {
-        mDimAmount = dimAmount;
-        updateBackgroundDimming();
-    }
-
-    private void animateDimmed(boolean dimmed) {
-        if (mDimAnimator != null) {
-            mDimAnimator.cancel();
-        }
-        float target = dimmed ? 1.0f : 0.0f;
-        if (target == mDimAmount) {
-            return;
-        }
-        mDimAnimator = TimeAnimator.ofFloat(mDimAmount, target);
-        mDimAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED);
-        mDimAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        mDimAnimator.addListener(mDimEndListener);
-        mDimAnimator.addUpdateListener(mDimUpdateListener);
-        mDimAnimator.start();
-    }
-
     void updateSensitiveness(boolean animate, boolean hideSensitive) {
         if (hideSensitive != mAmbientState.isHideSensitive()) {
             int childCount = getChildCount();
@@ -4564,7 +4183,6 @@
 
         runAnimationFinishedRunnables();
         setAnimationRunning(false);
-        updateBackground();
         updateViewShadows();
     }
 
@@ -4714,7 +4332,6 @@
             invalidateOutline();
         }
         updateAlgorithmHeightAndPadding();
-        updateBackgroundDimming();
         requestChildrenUpdate();
         updateOwnTranslationZ();
     }
@@ -4747,21 +4364,6 @@
         }
     }
 
-    private int getNotGoneIndex(View child) {
-        int count = getChildCount();
-        int notGoneIndex = 0;
-        for (int i = 0; i < count; i++) {
-            View v = getChildAt(i);
-            if (child == v) {
-                return notGoneIndex;
-            }
-            if (v.getVisibility() != View.GONE) {
-                notGoneIndex++;
-            }
-        }
-        return -1;
-    }
-
     /**
      * Returns whether or not a History button is shown in the footer. If there is no footer, then
      * this will return false.
@@ -5266,13 +4868,10 @@
     void onStatePostChange(boolean fromShadeLocked) {
         boolean onKeyguard = onKeyguard();
 
-        mAmbientState.setDimmed(onKeyguard);
-
         if (mHeadsUpAppearanceController != null) {
             mHeadsUpAppearanceController.onStateChanged();
         }
 
-        setDimmed(onKeyguard, fromShadeLocked);
         setExpandingEnabled(!onKeyguard);
         if (!FooterViewRefactor.isEnabled()) {
             updateFooter();
@@ -5676,7 +5275,6 @@
      */
     public void setDozeAmount(float dozeAmount) {
         mAmbientState.setDozeAmount(dozeAmount);
-        updateContinuousBackgroundDrawing();
         updateStackPosition();
         requestChildrenUpdate();
     }
@@ -5711,7 +5309,6 @@
                 view.setTranslationY(wakeUplocation);
             }
         }
-        mDimmedNeedsAnimation = true;
     }
 
     void setAnimateBottomOnLayout(boolean animateBottomOnLayout) {
@@ -5763,7 +5360,6 @@
         updateFirstAndLastBackgroundViews();
         requestDisallowInterceptTouchEvent(true);
         updateContinuousShadowDrawing();
-        updateContinuousBackgroundDrawing();
         requestChildrenUpdate();
     }
 
@@ -5786,7 +5382,6 @@
      * @param numHeadsUp the number of active alerting notifications.
      */
     public void setNumHeadsUp(long numHeadsUp) {
-        mNumHeadsUp = numHeadsUp;
         mAmbientState.setHasHeadsUpEntries(numHeadsUp > 0);
     }
 
@@ -6160,19 +5755,6 @@
         mSpeedBumpIndexDirty = true;
     }
 
-    void updateContinuousBackgroundDrawing() {
-        boolean continuousBackground = !mAmbientState.isFullyAwake()
-                && mSwipeHelper.isSwiping();
-        if (continuousBackground != mContinuousBackgroundUpdate) {
-            mContinuousBackgroundUpdate = continuousBackground;
-            if (continuousBackground) {
-                getViewTreeObserver().addOnPreDrawListener(mBackgroundUpdater);
-            } else {
-                getViewTreeObserver().removeOnPreDrawListener(mBackgroundUpdater);
-            }
-        }
-    }
-
     private void resetAllSwipeState() {
         Trace.beginSection("NSSL.resetAllSwipeState()");
         mSwipeHelper.resetTouchState();
@@ -6259,7 +5841,6 @@
                         .animateHeight()
                         .animateTopInset()
                         .animateY()
-                        .animateDimmed()
                         .animateZ(),
 
                 // ANIMATION_TYPE_ACTIVATED_CHILD
@@ -6267,8 +5848,7 @@
                         .animateZ(),
 
                 // ANIMATION_TYPE_DIMMED
-                new AnimationFilter()
-                        .animateDimmed(),
+                new AnimationFilter(),
 
                 // ANIMATION_TYPE_CHANGE_POSITION
                 new AnimationFilter()
@@ -6283,7 +5863,6 @@
                         .animateHeight()
                         .animateTopInset()
                         .animateY()
-                        .animateDimmed()
                         .animateZ()
                         .hasDelays(),
 
@@ -6339,7 +5918,6 @@
                 // ANIMATION_TYPE_EVERYTHING
                 new AnimationFilter()
                         .animateAlpha()
-                        .animateDimmed()
                         .animateHideSensitive()
                         .animateHeight()
                         .animateTopInset()
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 7c13877..3bdd0e9 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
@@ -875,8 +875,6 @@
         mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
         mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener);
 
-        mScrimController.setScrimBehindChangeRunnable(mView::updateBackgroundDimming);
-
         mLockscreenShadeTransitionController.setStackScroller(this);
 
         mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener);
@@ -1743,13 +1741,6 @@
     }
 
     /**
-     * Set the dimmed state for all of the notification views.
-     */
-    public void setDimmed(boolean dimmed, boolean animate) {
-        mView.setDimmed(dimmed, animate);
-    }
-
-    /**
      * @return the inset during the full shade transition, that needs to be added to the position
      * of the quick settings edge. This is relevant for media, that is transitioning
      * from the keyguard host to the quick settings one.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 1ef9a8f..9b1952b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -369,13 +369,11 @@
     /** Updates the dimmed and hiding sensitive states of the children. */
     private void updateDimmedAndHideSensitive(AmbientState ambientState,
             StackScrollAlgorithmState algorithmState) {
-        boolean dimmed = ambientState.isDimmed();
         boolean hideSensitive = ambientState.isHideSensitive();
         int childCount = algorithmState.visibleChildren.size();
         for (int i = 0; i < childCount; i++) {
             ExpandableView child = algorithmState.visibleChildren.get(i);
             ExpandableViewState childViewState = child.getViewState();
-            childViewState.dimmed = dimmed;
             childViewState.hideSensitive = hideSensitive;
         }
     }
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 f523793..78fc147 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
@@ -42,8 +42,10 @@
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
@@ -99,7 +101,9 @@
     private val alternateBouncerToGoneTransitionViewModel:
         AlternateBouncerToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+    private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
+    private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
     private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
     private val glanceableHubToLockscreenTransitionViewModel:
         GlanceableHubToLockscreenTransitionViewModel,
@@ -155,8 +159,8 @@
             .distinctUntilChanged()
             .dumpWhileCollecting("isShadeLocked")
 
-    private val shadeCollapseFadeInComplete = MutableStateFlow(false)
-            .dumpValue("shadeCollapseFadeInComplete")
+    private val shadeCollapseFadeInComplete =
+        MutableStateFlow(false).dumpValue("shadeCollapseFadeInComplete")
 
     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
         interactor.configurationBasedDimensions
@@ -187,9 +191,8 @@
                     statesForConstrainedNotifications.contains(it)
                 },
                 keyguardTransitionInteractor
-                    .transitionValue(LOCKSCREEN)
-                    .onStart { emit(0f) }
-                    .map { it > 0 }
+                    .isInTransitionWhere { from, to -> from == LOCKSCREEN || to == LOCKSCREEN }
+                    .onStart { emit(false) }
             ) { constrainedNotificationState, transitioningToOrFromLockscreen ->
                 constrainedNotificationState || transitioningToOrFromLockscreen
             }
@@ -362,16 +365,16 @@
 
     private val alphaWhenGoneAndShadeState: Flow<Float> =
         combineTransform(
-            keyguardTransitionInteractor.transitions
-                .map { step -> step.to == GONE && step.transitionState == FINISHED }
-                .distinctUntilChanged(),
-            keyguardInteractor.statusBarState,
-        ) { isGoneTransitionFinished, statusBarState ->
-            if (isGoneTransitionFinished && statusBarState == SHADE) {
-                emit(1f)
+                keyguardTransitionInteractor.transitions
+                    .map { step -> step.to == GONE && step.transitionState == FINISHED }
+                    .distinctUntilChanged(),
+                keyguardInteractor.statusBarState,
+            ) { isGoneTransitionFinished, statusBarState ->
+                if (isGoneTransitionFinished && statusBarState == SHADE) {
+                    emit(1f)
+                }
             }
-        }
-        .dumpWhileCollecting("alphaWhenGoneAndShadeState")
+            .dumpWhileCollecting("alphaWhenGoneAndShadeState")
 
     fun expansionAlpha(viewState: ViewStateAccessor): Flow<Float> {
         // All transition view models are mututally exclusive, and safe to merge
@@ -379,7 +382,9 @@
             merge(
                 alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
                 aodToLockscreenTransitionViewModel.notificationAlpha,
+                aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                 dozingToLockscreenTransitionViewModel.lockscreenAlpha,
+                dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                 dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
                 goneToAodTransitionViewModel.notificationAlpha,
                 goneToDreamingTransitionViewModel.lockscreenAlpha,
@@ -433,30 +438,35 @@
      * alpha sources.
      */
     val glanceableHubAlpha: Flow<Float> =
-        isOnGlanceableHubWithoutShade.flatMapLatest { isOnGlanceableHubWithoutShade ->
-            combineTransform(
-                lockscreenToGlanceableHubRunning,
-                glanceableHubToLockscreenRunning,
-                merge(
-                    lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
-                    glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
-                )
-            ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha ->
-                if (isOnGlanceableHubWithoutShade) {
-                    // Notifications should not be visible on the glanceable hub.
-                    // TODO(b/321075734): implement a way to actually set the notifications to gone
-                    //  while on the hub instead of just adjusting alpha
-                    emit(0f)
-                } else if (lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning) {
-                    emit(alpha)
-                } else {
-                    // Not on the hub and no transitions running, return full visibility so we don't
-                    // block the notifications from showing.
-                    emit(1f)
+        isOnGlanceableHubWithoutShade
+            .flatMapLatest { isOnGlanceableHubWithoutShade ->
+                combineTransform(
+                    lockscreenToGlanceableHubRunning,
+                    glanceableHubToLockscreenRunning,
+                    merge(
+                        lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
+                        glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
+                    )
+                ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha ->
+                    if (isOnGlanceableHubWithoutShade) {
+                        // Notifications should not be visible on the glanceable hub.
+                        // TODO(b/321075734): implement a way to actually set the notifications to
+                        // gone
+                        //  while on the hub instead of just adjusting alpha
+                        emit(0f)
+                    } else if (
+                        lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning
+                    ) {
+                        emit(alpha)
+                    } else {
+                        // Not on the hub and no transitions running, return full visibility so we
+                        // don't
+                        // block the notifications from showing.
+                        emit(1f)
+                    }
                 }
             }
-        }
-        .dumpWhileCollecting("glanceableHubAlpha")
+            .dumpWhileCollecting("glanceableHubAlpha")
 
     /**
      * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be
@@ -464,20 +474,20 @@
      */
     fun translationY(params: BurnInParameters): Flow<Float> {
         return combine(
-            aodBurnInViewModel.translationY(params).onStart { emit(0f) },
-            isOnLockscreenWithoutShade,
-            merge(
-                keyguardInteractor.keyguardTranslationY,
-                occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
-            )
-        ) { burnInY, isOnLockscreenWithoutShade, translationY ->
-            if (isOnLockscreenWithoutShade) {
-                burnInY + translationY
-            } else {
-                0f
+                aodBurnInViewModel.translationY(params).onStart { emit(0f) },
+                isOnLockscreenWithoutShade,
+                merge(
+                    keyguardInteractor.keyguardTranslationY,
+                    occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
+                )
+            ) { burnInY, isOnLockscreenWithoutShade, translationY ->
+                if (isOnLockscreenWithoutShade) {
+                    burnInY + translationY
+                } else {
+                    0f
+                }
             }
-        }
-        .dumpWhileCollecting("translationY")
+            .dumpWhileCollecting("translationY")
     }
 
     /**
@@ -486,10 +496,10 @@
      */
     val translationX: Flow<Float> =
         merge(
-            lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX,
-            glanceableHubToLockscreenTransitionViewModel.notificationTranslationX,
-        )
-        .dumpWhileCollecting("translationX")
+                lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX,
+                glanceableHubToLockscreenTransitionViewModel.notificationTranslationX,
+            )
+            .dumpWhileCollecting("translationX")
 
     /**
      * When on keyguard, there is limited space to display notifications so calculate how many could
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index d2e36b8..088f894 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -207,8 +207,6 @@
     private ScrimView mNotificationsScrim;
     private ScrimView mScrimBehind;
 
-    private Runnable mScrimBehindChangeRunnable;
-
     private final KeyguardStateController mKeyguardStateController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final DozeParameters mDozeParameters;
@@ -415,11 +413,6 @@
         behindScrim.enableBottomEdgeConcave(mClipsQsScrim);
         mNotificationsScrim.enableRoundedCorners(true);
 
-        if (mScrimBehindChangeRunnable != null) {
-            mScrimBehind.setChangeRunnable(mScrimBehindChangeRunnable, mMainExecutor);
-            mScrimBehindChangeRunnable = null;
-        }
-
         final ScrimState[] states = ScrimState.values();
         for (int i = 0; i < states.length; i++) {
             states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager);
@@ -1542,16 +1535,6 @@
         mScrimBehind.postOnAnimationDelayed(callback, 32 /* delayMillis */);
     }
 
-    public void setScrimBehindChangeRunnable(Runnable changeRunnable) {
-        // TODO: remove this. This is necessary because of an order-of-operations limitation.
-        // The fix is to move more of these class into @SysUISingleton.
-        if (mScrimBehind == null) {
-            mScrimBehindChangeRunnable = changeRunnable;
-        } else {
-            mScrimBehind.setChangeRunnable(changeRunnable, mMainExecutor);
-        }
-    }
-
     private void updateThemeColors() {
         if (mScrimBehind == null) return;
         int background = Utils.getColorAttr(mScrimBehind.getContext(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index ee84434..ba89d4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -95,6 +95,7 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.domain.interactor.StatusBarKeyguardViewManagerInteractor;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.unfold.FoldAodAnimationController;
@@ -351,8 +352,8 @@
     private Lazy<WindowManagerLockscreenVisibilityInteractor> mWmLockscreenVisibilityInteractor;
     private Lazy<KeyguardSurfaceBehindInteractor> mSurfaceBehindInteractor;
     private Lazy<KeyguardDismissActionInteractor> mKeyguardDismissActionInteractor;
-
     private final JavaAdapter mJavaAdapter;
+    private StatusBarKeyguardViewManagerInteractor mStatusBarKeyguardViewManagerInteractor;
 
     @Inject
     public StatusBarKeyguardViewManager(
@@ -386,7 +387,8 @@
             SelectedUserInteractor selectedUserInteractor,
             Lazy<KeyguardSurfaceBehindInteractor> surfaceBehindInteractor,
             JavaAdapter javaAdapter,
-            Lazy<SceneInteractor> sceneInteractorLazy
+            Lazy<SceneInteractor> sceneInteractorLazy,
+            StatusBarKeyguardViewManagerInteractor statusBarKeyguardViewManagerInteractor
     ) {
         mContext = context;
         mViewMediatorCallback = callback;
@@ -421,6 +423,7 @@
         mSurfaceBehindInteractor = surfaceBehindInteractor;
         mJavaAdapter = javaAdapter;
         mSceneInteractorLazy = sceneInteractorLazy;
+        mStatusBarKeyguardViewManagerInteractor = statusBarKeyguardViewManagerInteractor;
     }
 
     KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -503,6 +506,11 @@
                                     lockscreenVis || animatingSurface
                     ),
                     this::consumeShowStatusBarKeyguardView);
+
+            mJavaAdapter.alwaysCollectFlow(
+                    mStatusBarKeyguardViewManagerInteractor.getKeyguardViewOcclusionState(),
+                    (occlusionState) -> setOccluded(
+                            occlusionState.getOccluded(), occlusionState.getAnimate()));
         }
     }
 
@@ -1453,6 +1461,10 @@
             hideAlternateBouncer(false);
             executeAfterKeyguardGoneAction();
         }
+
+        if (KeyguardWmStateRefactor.isEnabled()) {
+            mKeyguardTransitionInteractor.startDismissKeyguardTransition();
+        }
     }
 
     /** Display security message to relevant KeyguardMessageArea. */
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
index 0128eb7..80ccd64 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
@@ -22,6 +22,24 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.onStart
 
+fun BatteryController.isDevicePluggedIn(): Flow<Boolean> {
+    return conflatedCallbackFlow {
+            val batteryCallback =
+                object : BatteryController.BatteryStateChangeCallback {
+                    override fun onBatteryLevelChanged(
+                        level: Int,
+                        pluggedIn: Boolean,
+                        charging: Boolean
+                    ) {
+                        trySend(pluggedIn)
+                    }
+                }
+            addCallback(batteryCallback)
+            awaitClose { removeCallback(batteryCallback) }
+        }
+        .onStart { emit(isPluggedIn) }
+}
+
 fun BatteryController.isBatteryPowerSaveEnabled(): Flow<Boolean> {
     return conflatedCallbackFlow {
             val batteryCallback =
@@ -35,3 +53,35 @@
         }
         .onStart { emit(isPowerSave) }
 }
+
+fun BatteryController.getBatteryLevel(): Flow<Int> {
+    return conflatedCallbackFlow {
+            val batteryCallback =
+                object : BatteryController.BatteryStateChangeCallback {
+                    override fun onBatteryLevelChanged(
+                        level: Int,
+                        pluggedIn: Boolean,
+                        charging: Boolean
+                    ) {
+                        trySend(level)
+                    }
+                }
+            addCallback(batteryCallback)
+            awaitClose { removeCallback(batteryCallback) }
+        }
+        .onStart { emit(0) }
+}
+
+fun BatteryController.isExtremePowerSaverEnabled(): Flow<Boolean> {
+    return conflatedCallbackFlow {
+            val batteryCallback =
+                object : BatteryController.BatteryStateChangeCallback {
+                    override fun onExtremeBatterySaverChanged(isExtreme: Boolean) {
+                        trySend(isExtreme)
+                    }
+                }
+            addCallback(batteryCallback)
+            awaitClose { removeCallback(batteryCallback) }
+        }
+        .onStart { emit(isExtremeSaverOn) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java
index 0d6c0f5..10cf082 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java
@@ -25,8 +25,11 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 
+import com.android.app.tracing.TraceUtils;
 import com.android.systemui.settings.UserTracker;
 
+import kotlin.Unit;
+
 /**
  * Used to interact with per-user Settings.Secure and Settings.System settings (but not
  * Settings.Global, since those do not vary per-user)
@@ -123,8 +126,16 @@
     default void registerContentObserverForUser(
             Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
             int userHandle) {
-        getContentResolver().registerContentObserver(
-                uri, notifyForDescendants, settingsObserver, getRealUserHandle(userHandle));
+        TraceUtils.trace(
+                () -> {
+                    // The limit for trace tags length is 127 chars, which leaves us 90 for Uri.
+                    return "USP#registerObserver#[" + uri.toString() + "]";
+                }, () -> {
+                    getContentResolver().registerContentObserver(
+                            uri, notifyForDescendants, settingsObserver,
+                            getRealUserHandle(userHandle));
+                    return Unit.INSTANCE;
+                });
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 4045630..deec215 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -120,7 +120,7 @@
 import com.android.systemui.haptics.slider.HapticSliderViewBinder;
 import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
 import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.VolumeDialog;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.plugins.VolumeDialogController.State;
@@ -265,7 +265,7 @@
     private final Object mSafetyWarningLock = new Object();
     private final Accessibility mAccessibility = new Accessibility();
     private final ConfigurationController mConfigurationController;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final MediaOutputDialogManager mMediaOutputDialogManager;
     private final CsdWarningDialog.Factory mCsdWarningDialogFactory;
     private final VolumePanelNavigationInteractor mVolumePanelNavigationInteractor;
     private final VolumeNavigator mVolumeNavigator;
@@ -316,7 +316,7 @@
             AccessibilityManagerWrapper accessibilityManagerWrapper,
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaOutputDialogManager mediaOutputDialogManager,
             InteractionJankMonitor interactionJankMonitor,
             VolumePanelNavigationInteractor volumePanelNavigationInteractor,
             VolumeNavigator volumeNavigator,
@@ -340,7 +340,7 @@
         mAccessibilityMgr = accessibilityManagerWrapper;
         mDeviceProvisionedController = deviceProvisionedController;
         mConfigurationController = configurationController;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaOutputDialogManager = mediaOutputDialogManager;
         mCsdWarningDialogFactory = csdWarningDialogFactory;
         mShowActiveStreamOnly = showActiveStreamOnly();
         mHasSeenODICaptionsTooltip =
@@ -1199,7 +1199,7 @@
             mSettingsIcon.setOnClickListener(v -> {
                 Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
                 dismissH(DISMISS_REASON_SETTINGS_CLICKED);
-                mMediaOutputDialogFactory.dismiss();
+                mMediaOutputDialogManager.dismiss();
                 mVolumeNavigator.openVolumePanel(
                         mVolumePanelNavigationInteractor.getVolumePanelRoute());
             });
@@ -2082,6 +2082,9 @@
                 } else {
                     row.anim.cancel();
                     row.anim.setIntValues(progress, newProgress);
+                    // The animator can't keep up with the volume changes so haptics need to be
+                    // triggered here. This happens when the volume keys are continuously pressed.
+                    row.deliverOnProgressChangedHaptics(false, newProgress);
                 }
                 row.animTargetProgress = newProgress;
                 row.anim.setDuration(UPDATE_ANIMATION_DURATION);
@@ -2486,10 +2489,10 @@
             if (getActiveRow().equals(mRow)
                     && mRow.slider.getVisibility() == VISIBLE
                     && mRow.mHapticPlugin != null) {
-                mRow.mHapticPlugin.onProgressChanged(seekBar, progress, fromUser);
-                if (!fromUser) {
-                    // Consider a change from program as the volume key being continuously pressed
-                    mRow.mHapticPlugin.onKeyDown();
+                if (fromUser || mRow.animTargetProgress == progress) {
+                    // Deliver user-generated slider changes immediately, or when the animation
+                    // completes
+                    mRow.deliverOnProgressChangedHaptics(fromUser, progress);
                 }
             }
             if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream)
@@ -2571,11 +2574,11 @@
                 /* progressInterpolatorFactor= */ 1f,
                 /* progressBasedDragMinScale= */ 0f,
                 /* progressBasedDragMaxScale= */ 0.2f,
-                /* additionalVelocityMaxBump= */ 0.15f,
+                /* additionalVelocityMaxBump= */ 0.25f,
                 /* deltaMillisForDragInterval= */ 0f,
-                /* deltaProgressForDragThreshold= */ 0.015f,
-                /* numberOfLowTicks= */ 5,
-                /* maxVelocityToScale= */ 300f,
+                /* deltaProgressForDragThreshold= */ 0.05f,
+                /* numberOfLowTicks= */ 4,
+                /* maxVelocityToScale= */ 200,
                 /* velocityAxis= */ MotionEvent.AXIS_Y,
                 /* upperBookendScale= */ 1f,
                 /* lowerBookendScale= */ 0.05f,
@@ -2642,6 +2645,14 @@
         void removeHaptics() {
             slider.setOnTouchListener(null);
         }
+
+        void deliverOnProgressChangedHaptics(boolean fromUser, int progress) {
+            mHapticPlugin.onProgressChanged(slider, progress, fromUser);
+            if (!fromUser) {
+                // Consider a change from program as the volume key being continuously pressed
+                mHapticPlugin.onKeyDown();
+            }
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 64a5644..1f87ec8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -24,7 +24,7 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.VolumeDialog;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -100,7 +100,7 @@
             AccessibilityManagerWrapper accessibilityManagerWrapper,
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaOutputDialogManager mediaOutputDialogManager,
             InteractionJankMonitor interactionJankMonitor,
             VolumePanelNavigationInteractor volumePanelNavigationInteractor,
             VolumeNavigator volumeNavigator,
@@ -116,7 +116,7 @@
                 accessibilityManagerWrapper,
                 deviceProvisionedController,
                 configurationController,
-                mediaOutputDialogFactory,
+                mediaOutputDialogManager,
                 interactionJankMonitor,
                 volumePanelNavigationInteractor,
                 volumeNavigator,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
index 2ab5998..cb16abe 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.media.dialog.MediaOutputDialogManager
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import javax.inject.Inject
@@ -30,20 +30,22 @@
 class MediaOutputActionsInteractor
 @Inject
 constructor(
-    private val mediaOutputDialogFactory: MediaOutputDialogFactory,
+    private val mediaOutputDialogManager: MediaOutputDialogManager,
 ) {
 
     fun onBarClick(session: MediaDeviceSession, expandable: Expandable) {
         when (session) {
             is MediaDeviceSession.Active -> {
-                mediaOutputDialogFactory.createWithController(
+                mediaOutputDialogManager.createAndShowWithController(
                     session.packageName,
                     false,
                     expandable.dialogController()
                 )
             }
             is MediaDeviceSession.Inactive -> {
-                mediaOutputDialogFactory.createDialogForSystemRouting(expandable.dialogController())
+                mediaOutputDialogManager.createAndShowForSystemRouting(
+                    expandable.dialogController()
+                )
             }
             else -> {
                 /* do nothing */
@@ -56,7 +58,7 @@
             cuj =
                 DialogCuj(
                     InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                    MediaOutputDialogFactory.INTERACTION_JANK_TAG
+                    MediaOutputDialogManager.INTERACTION_JANK_TAG
                 )
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
index f07932c..e3be3822 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
@@ -19,6 +19,7 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.core.animation.doOnEnd
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.doOnEnd
@@ -30,6 +31,7 @@
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 @RunWithLooper
+@FlakyTest(bugId = 302149604)
 class AnimatorTestRuleOrderTest : SysuiTestCase() {
 
     @get:Rule val animatorTestRule = AnimatorTestRule(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index 043dcaa..3c073d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -193,6 +193,34 @@
     }
 
     @Test
+    @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
+    fun modeEstimate_batteryPercentView_isNotNull_flagOn() {
+        mBatteryMeterView.onBatteryLevelChanged(15, false)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        // New battery icon only uses the percent view for the estimate text
+        assertThat(mBatteryMeterView.batteryPercentView).isNotNull()
+        // Make sure that it was added to the view hierarchy
+        assertThat(mBatteryMeterView.batteryPercentView.parent).isNotNull()
+    }
+
+    @Test
+    @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
+    fun modePercent_batteryPercentView_isNull_flagOn() {
+        mBatteryMeterView.onBatteryLevelChanged(15, false)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        // New battery icon only uses the percent view for the estimate text
+        assertThat(mBatteryMeterView.batteryPercentView).isNull()
+    }
+
+    @Test
     fun contentDescription_manyUpdates_alwaysUpdated() {
         // BatteryDefender
         mBatteryMeterView.onBatteryLevelChanged(90, false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
index 7a18477..a569cee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
@@ -42,7 +42,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -77,7 +77,8 @@
     @Mock SysUiState mSysUiState;
     @Mock
     DialogTransitionAnimator mDialogTransitionAnimator;
-    @Mock MediaOutputDialogFactory mMediaOutputDialogFactory;
+    @Mock
+    MediaOutputDialogManager mMediaOutputDialogManager;
     private SystemUIDialog mDialog;
     private TextView mTitle;
     private TextView mSubTitle;
@@ -96,7 +97,7 @@
 
         mBroadcastDialogDelegate = new BroadcastDialogDelegate(
                 mContext,
-                mMediaOutputDialogFactory,
+                mMediaOutputDialogManager,
                 mLocalBluetoothManager,
                 new UiEventLoggerFake(),
                 mFakeExecutor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index 489665c..51828c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -6,6 +6,7 @@
 import android.graphics.Point
 import android.graphics.Rect
 import android.os.PowerManager
+import android.platform.test.annotations.DisableFlags
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.RemoteAnimationTarget
@@ -15,6 +16,7 @@
 import android.view.ViewRootImpl
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardViewController
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
@@ -130,6 +132,7 @@
      * surface, or the user will see the wallpaper briefly as the app animates in.
      */
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun noSurfaceAnimation_ifWakeAndUnlocking() {
         whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
 
@@ -320,6 +323,7 @@
      * If we are not wake and unlocking, we expect the unlock animation to play normally.
      */
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun surfaceAnimation_multipleTargets() {
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                 arrayOf(remoteTarget1, remoteTarget2),
@@ -358,6 +362,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun surfaceBehindAlphaOverriddenTo0_ifNotInteractive() {
         whenever(powerManager.isInteractive).thenReturn(false)
 
@@ -389,6 +394,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun surfaceBehindAlphaNotOverriddenTo0_ifInteractive() {
         whenever(powerManager.isInteractive).thenReturn(true)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 0957748..0bd4cbe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -1263,7 +1263,8 @@
                 mSystemPropertiesHelper,
                 () -> mock(WindowManagerLockscreenVisibilityManager.class),
                 mSelectedUserInteractor,
-                mKeyguardInteractor);
+                mKeyguardInteractor,
+                mock(WindowManagerOcclusionManager.class));
         mViewMediator.start();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
new file mode 100644
index 0000000..7bef01a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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.os.PowerManager
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
+import junit.framework.Assert.assertEquals
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromAodTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fromAodTransitionInteractor
+
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Before
+    fun setup() {
+        underTest.start()
+
+        // Transition to AOD and set the power interactor asleep.
+        powerInteractor.setAsleepForTest()
+        runBlocking {
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope
+            )
+            kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+            reset(transitionRepository)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Under default conditions, we should transition to LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() =
+        testScope.runTest {
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissableKeyguard() =
+        testScope.runTest {
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissableKeyguard() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromGone() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're GONE.
+            assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState())
+
+            // Get part way to AOD.
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetectedAfterFinishedInAod_fromGone() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're GONE.
+            assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState())
+
+            // Get all the way to AOD
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should go to OCCLUDED - we came from GONE, but we finished in AOD, so this is no
+            // longer an insecure camera launch and it would be bad if we unlocked now.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromLockscreen() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're in LOCKSCREEN.
+            assertEquals(
+                KeyguardState.LOCKSCREEN,
+                kosmos.keyguardTransitionInteractor.getFinishedState()
+            )
+
+            // Get part way to AOD.
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testWakeAndUnlock_transitionsToGone_onlyAfterDismissCallPostWakeup() =
+        testScope.runTest {
+            kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Waking up from wake and unlock should not start any transitions, we'll wait for the
+            // dismiss call.
+            assertThat(transitionRepository).noTransitionsStarted()
+
+            underTest.dismissAod()
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
new file mode 100644
index 0000000..258dbf3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -0,0 +1,312 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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.os.PowerManager
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
+import junit.framework.Assert.assertEquals
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromDozingTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fromDozingTransitionInteractor
+
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Before
+    fun setup() {
+        underTest.start()
+
+        // Transition to DOZING and set the power interactor asleep.
+        powerInteractor.setAsleepForTest()
+        runBlocking {
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DOZING,
+                testScope
+            )
+            kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+            reset(transitionRepository)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Under default conditions, we should transition to LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGlanceableHub_onWakeup_ifIdleOnCommunal_noOccludingActivity() =
+        testScope.runTest {
+            kosmos.fakeCommunalRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+            runCurrent()
+
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Under default conditions, we should transition to LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() =
+        testScope.runTest {
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop_evenIfIdleOnCommunal() =
+        testScope.runTest {
+            kosmos.fakeCommunalRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+            runCurrent()
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissableKeyguard() =
+        testScope.runTest {
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissableKeyguard() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.GONE)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromGone() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.DOZING,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're GONE.
+            assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState())
+
+            // Get part way to AOD.
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.DOZING,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.GONE)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetectedAfterFinishedInAod_fromGone() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.DOZING,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're GONE.
+            assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState())
+
+            // Get all the way to AOD
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.DOZING,
+                testScope = testScope,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should go to OCCLUDED - we came from GONE, but we finished in AOD, so this is no
+            // longer an insecure camera launch and it would be bad if we unlocked now.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromLockscreen() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.DOZING,
+                to = KeyguardState.LOCKSCREEN,
+                testScope,
+            )
+            runCurrent()
+
+            // Make sure we're in LOCKSCREEN.
+            assertEquals(
+                KeyguardState.LOCKSCREEN,
+                kosmos.keyguardTransitionInteractor.getFinishedState()
+            )
+
+            // Get part way to AOD.
+            powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN)
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DOZING,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            // Detect a power gesture and then wake up.
+            reset(transitionRepository)
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // We should head back to GONE since we started there.
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
new file mode 100644
index 0000000..f534ba5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.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.
+ */
+
+/*
+ * 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.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromDreamingTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fromDreamingTransitionInteractor
+
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Before
+    fun setup() {
+        underTest.start()
+
+        // Transition to DOZING and set the power interactor asleep.
+        powerInteractor.setAsleepForTest()
+        runBlocking {
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope
+            )
+            kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+            reset(transitionRepository)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_ifDreamEnds_occludingActivityOnTop() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setDreaming(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope,
+            )
+            runCurrent()
+
+            reset(transitionRepository)
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            kosmos.fakeKeyguardRepository.setDreaming(false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DREAMING,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testDoesNotTransitionToOccluded_occludingActivityOnTop_whileStillDreaming() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setDreaming(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope,
+            )
+            runCurrent()
+
+            reset(transitionRepository)
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            runCurrent()
+
+            assertThat(transitionRepository).noTransitionsStarted()
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionsToLockscreen_whenOccludingActivityEnds() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setDreaming(true)
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.DREAMING,
+                testScope,
+            )
+            runCurrent()
+
+            reset(transitionRepository)
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DREAMING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index 6aebe36..c3e24d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.app.ActivityManager
+import android.app.WindowConfiguration
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -26,6 +28,7 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -42,6 +45,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 
@@ -171,4 +175,49 @@
 
             assertThatRepository(transitionRepository).noTransitionsStarted()
         }
+
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionsToOccluded_whenShowWhenLockedActivityOnTop() =
+        testScope.runTest {
+            underTest.start()
+            runCurrent()
+
+            reset(transitionRepository)
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(
+                true,
+                ActivityManager.RunningTaskInfo().apply {
+                    topActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD
+                }
+            )
+            runCurrent()
+
+            assertThatRepository(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionsToDream_whenDreamActivityOnTop() =
+        testScope.runTest {
+            underTest.start()
+            runCurrent()
+
+            reset(transitionRepository)
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(
+                true,
+                ActivityManager.RunningTaskInfo().apply {
+                    topActivityType = WindowConfiguration.ACTIVITY_TYPE_DREAM
+                }
+            )
+            runCurrent()
+
+            assertThatRepository(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.DREAMING,
+                )
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt
new file mode 100644
index 0000000..d3c4848
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * 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.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.testKosmos
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FromOccludedTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.fromOccludedTransitionInteractor
+
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Before
+    fun setup() {
+        underTest.start()
+
+        // Transition to OCCLUDED and set up PowerInteractor and the occlusion repository.
+        powerInteractor.setAwakeForTest()
+        runBlocking {
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.OCCLUDED,
+                testScope
+            )
+            reset(transitionRepository)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testShowWhenLockedActivity_noLongerOnTop_transitionsToLockscreen() =
+        testScope.runTest {
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.OCCLUDED,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testShowWhenLockedActivity_noLongerOnTop_transitionsToGlanceableHub_ifIdleOnCommunal() =
+        testScope.runTest {
+            kosmos.fakeCommunalRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+            runCurrent()
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.OCCLUDED,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                )
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
index f33a5c9..7ee8963 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
@@ -16,14 +16,23 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
 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.data.repository.keyguardOcclusionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
@@ -31,20 +40,27 @@
 import junit.framework.Assert.assertTrue
 import junit.framework.Assert.fail
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class FromPrimaryBouncerTransitionInteractorTest : SysuiTestCase() {
-    val kosmos = testKosmos()
+    val kosmos =
+        testKosmos().apply {
+            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
+        }
     val underTest = kosmos.fromPrimaryBouncerTransitionInteractor
     val testScope = kosmos.testScope
     val selectedUserInteractor = kosmos.selectedUserInteractor
     val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+    val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
 
     @Test
     fun testSurfaceBehindVisibility() =
@@ -193,4 +209,85 @@
                 fail("surfaceBehindModel was unexpectedly null.")
             }
         }
+
+    @Test
+    @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testReturnToLockscreen_whenBouncerHides() =
+        testScope.runTest {
+            underTest.start()
+            bouncerRepository.setPrimaryShow(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.PRIMARY_BOUNCER,
+                testScope
+            )
+
+            reset(transitionRepository)
+
+            bouncerRepository.setPrimaryShow(false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.LOCKSCREEN
+                )
+        }
+
+    @Test
+    @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testReturnToGlanceableHub_whenBouncerHides_ifIdleOnCommunal() =
+        testScope.runTest {
+            underTest.start()
+            kosmos.fakeCommunalRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+            bouncerRepository.setPrimaryShow(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.PRIMARY_BOUNCER,
+                testScope
+            )
+
+            reset(transitionRepository)
+
+            bouncerRepository.setPrimaryShow(false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GLANCEABLE_HUB
+                )
+        }
+
+    @Test
+    @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToOccluded_bouncerHide_occludingActivityOnTop() =
+        testScope.runTest {
+            underTest.start()
+            bouncerRepository.setPrimaryShow(true)
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.PRIMARY_BOUNCER,
+                testScope
+            )
+
+            reset(transitionRepository)
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            runCurrent()
+
+            // Shouldn't transition to OCCLUDED until the bouncer hides.
+            assertThat(transitionRepository).noTransitionsStarted()
+
+            bouncerRepository.setPrimaryShow(false)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.OCCLUDED
+                )
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
new file mode 100644
index 0000000..8a77ed2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
@@ -0,0 +1,224 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4
+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.keyguardOcclusionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import kotlin.test.Test
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardOcclusionInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.keyguardOcclusionInteractor
+    private val powerInteractor = kosmos.powerInteractor
+    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    @Test
+    fun testTransitionFromPowerGesture_whileGoingToSleep_isTrue() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            runCurrent()
+
+            assertTrue(underTest.shouldTransitionFromPowerButtonGesture())
+        }
+
+    @Test
+    fun testTransitionFromPowerGesture_whileAsleep_isTrue() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            runCurrent()
+
+            assertTrue(underTest.shouldTransitionFromPowerButtonGesture())
+        }
+
+    @Test
+    fun testTransitionFromPowerGesture_whileWaking_isFalse() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            runCurrent()
+
+            assertFalse(underTest.shouldTransitionFromPowerButtonGesture())
+        }
+
+    @Test
+    fun testTransitionFromPowerGesture_whileAwake_isFalse() =
+        testScope.runTest {
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope = testScope,
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            runCurrent()
+
+            assertFalse(underTest.shouldTransitionFromPowerButtonGesture())
+        }
+
+    @Test
+    fun testShowWhenLockedActivityLaunchedFromPowerGesture_notTrueSecondTime() =
+        testScope.runTest {
+            val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture)
+            powerInteractor.setAsleepForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactly(
+                    false,
+                    true,
+                )
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactly(
+                    false,
+                    true,
+                    false,
+                )
+
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactly(
+                    false,
+                    true,
+                    // Power button gesture was not triggered a second time, so this should remain
+                    // false.
+                    false,
+                )
+        }
+
+    @Test
+    fun testShowWhenLockedActivityLaunchedFromPowerGesture_falseIfReturningToGone() =
+        testScope.runTest {
+            val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture)
+            powerInteractor.setAwakeForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                testScope = testScope,
+            )
+
+            powerInteractor.setAsleepForTest()
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING
+            )
+
+            powerInteractor.onCameraLaunchGestureDetected()
+            kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.GONE,
+                testScope = testScope,
+            )
+
+            assertThat(values)
+                .containsExactly(
+                    false,
+                )
+        }
+}
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 92aad69..95606ae 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
@@ -21,8 +21,8 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.systemui.Flags
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
-import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
@@ -40,7 +40,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
-import com.android.systemui.kosmos.testDispatcher
 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
@@ -48,7 +47,6 @@
 import com.android.systemui.shade.data.repository.fakeShadeRepository
 import com.android.systemui.statusbar.commandQueue
 import com.android.systemui.testKosmos
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -92,30 +90,26 @@
     private var commandQueue = kosmos.fakeCommandQueue
     private val shadeRepository = kosmos.fakeShadeRepository
     private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val transitionInteractor = kosmos.keyguardTransitionInteractor
     private lateinit var featureFlags: FakeFeatureFlags
 
     // Used to verify transition requests for test output
     @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
-    @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
 
     private val fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor
-    private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
-    private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor
-    private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor
-    private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor
-    private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor
-    private lateinit var fromAlternateBouncerTransitionInteractor:
-        FromAlternateBouncerTransitionInteractor
+    private val fromDreamingTransitionInteractor = kosmos.fromDreamingTransitionInteractor
+    private val fromDozingTransitionInteractor = kosmos.fromDozingTransitionInteractor
+    private val fromOccludedTransitionInteractor = kosmos.fromOccludedTransitionInteractor
+    private val fromGoneTransitionInteractor = kosmos.fromGoneTransitionInteractor
+    private val fromAodTransitionInteractor = kosmos.fromAodTransitionInteractor
+    private val fromAlternateBouncerTransitionInteractor =
+        kosmos.fromAlternateBouncerTransitionInteractor
     private val fromPrimaryBouncerTransitionInteractor =
         kosmos.fromPrimaryBouncerTransitionInteractor
-    private lateinit var fromDreamingLockscreenHostedTransitionInteractor:
-        FromDreamingLockscreenHostedTransitionInteractor
-    private lateinit var fromGlanceableHubTransitionInteractor:
-        FromGlanceableHubTransitionInteractor
+    private val fromDreamingLockscreenHostedTransitionInteractor =
+        kosmos.fromDreamingLockscreenHostedTransitionInteractor
+    private val fromGlanceableHubTransitionInteractor = kosmos.fromGlanceableHubTransitionInteractor
 
     private val powerInteractor = kosmos.powerInteractor
-    private val keyguardInteractor = kosmos.keyguardInteractor
     private val communalInteractor = kosmos.communalInteractor
 
     @Before
@@ -125,122 +119,21 @@
         whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
 
         mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+        mSetFlagsRule.disableFlags(
+            Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR,
+        )
         featureFlags = FakeFeatureFlags()
 
-        val glanceableHubTransitions =
-            GlanceableHubTransitions(
-                bgDispatcher = kosmos.testDispatcher,
-                transitionInteractor = transitionInteractor,
-                transitionRepository = transitionRepository,
-                communalInteractor = communalInteractor
-            )
-
         fromLockscreenTransitionInteractor.start()
         fromPrimaryBouncerTransitionInteractor.start()
-
-        fromDreamingTransitionInteractor =
-            FromDreamingTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    glanceableHubTransitions = glanceableHubTransitions,
-                )
-                .apply { start() }
-
-        fromDreamingLockscreenHostedTransitionInteractor =
-            FromDreamingLockscreenHostedTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                )
-                .apply { start() }
-
-        fromAodTransitionInteractor =
-            FromAodTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                )
-                .apply { start() }
-
-        fromGoneTransitionInteractor =
-            FromGoneTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                    communalInteractor = communalInteractor,
-                )
-                .apply { start() }
-
-        fromDozingTransitionInteractor =
-            FromDozingTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                    communalInteractor = communalInteractor,
-                )
-                .apply { start() }
-
-        fromOccludedTransitionInteractor =
-            FromOccludedTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                    communalInteractor = communalInteractor,
-                )
-                .apply { start() }
-
-        fromAlternateBouncerTransitionInteractor =
-            FromAlternateBouncerTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    communalInteractor = communalInteractor,
-                    powerInteractor = powerInteractor,
-                )
-                .apply { start() }
-
-        fromGlanceableHubTransitionInteractor =
-            FromGlanceableHubTransitionInteractor(
-                    scope = testScope,
-                    bgDispatcher = kosmos.testDispatcher,
-                    mainDispatcher = kosmos.testDispatcher,
-                    glanceableHubTransitions = glanceableHubTransitions,
-                    keyguardInteractor = keyguardInteractor,
-                    transitionRepository = transitionRepository,
-                    transitionInteractor = transitionInteractor,
-                    powerInteractor = powerInteractor,
-                )
-                .apply { start() }
-
-        mSetFlagsRule.disableFlags(
-            FLAG_KEYGUARD_WM_STATE_REFACTOR,
-        )
+        fromDreamingTransitionInteractor.start()
+        fromDreamingLockscreenHostedTransitionInteractor.start()
+        fromAodTransitionInteractor.start()
+        fromGoneTransitionInteractor.start()
+        fromDozingTransitionInteractor.start()
+        fromOccludedTransitionInteractor.start()
+        fromAlternateBouncerTransitionInteractor.start()
+        fromGlanceableHubTransitionInteractor.start()
     }
 
     @Test
@@ -257,7 +150,9 @@
                 .startedTransition(
                     to = KeyguardState.PRIMARY_BOUNCER,
                     from = KeyguardState.LOCKSCREEN,
-                    ownerName = "FromLockscreenTransitionInteractor",
+                    ownerName =
+                        "FromLockscreenTransitionInteractor" +
+                            "(#listenForLockscreenToPrimaryBouncer)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -282,7 +177,7 @@
                 .startedTransition(
                     to = KeyguardState.DOZING,
                     from = KeyguardState.OCCLUDED,
-                    ownerName = "FromOccludedTransitionInteractor",
+                    ownerName = "FromOccludedTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -307,7 +202,7 @@
                 .startedTransition(
                     to = KeyguardState.AOD,
                     from = KeyguardState.OCCLUDED,
-                    ownerName = "FromOccludedTransitionInteractor",
+                    ownerName = "FromOccludedTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -389,7 +284,7 @@
                 .startedTransition(
                     to = KeyguardState.DOZING,
                     from = KeyguardState.LOCKSCREEN,
-                    ownerName = "FromLockscreenTransitionInteractor",
+                    ownerName = "FromLockscreenTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -414,7 +309,7 @@
                 .startedTransition(
                     to = KeyguardState.AOD,
                     from = KeyguardState.LOCKSCREEN,
-                    ownerName = "FromLockscreenTransitionInteractor",
+                    ownerName = "FromLockscreenTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -778,7 +673,7 @@
                 .startedTransition(
                     to = KeyguardState.DOZING,
                     from = KeyguardState.GONE,
-                    ownerName = "FromGoneTransitionInteractor",
+                    ownerName = "FromGoneTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -803,7 +698,7 @@
                 .startedTransition(
                     to = KeyguardState.AOD,
                     from = KeyguardState.GONE,
-                    ownerName = "FromGoneTransitionInteractor",
+                    ownerName = "FromGoneTransitionInteractor(Sleep transition triggered)",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -1070,12 +965,14 @@
     @Test
     fun primaryBouncerToAod() =
         testScope.runTest {
+            // GIVEN aod available
+            keyguardRepository.setAodAvailable(true)
+            runCurrent()
+
             // GIVEN a prior transition has run to PRIMARY_BOUNCER
             bouncerRepository.setPrimaryShow(true)
             runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER)
 
-            // GIVEN aod available and starting to sleep
-            keyguardRepository.setAodAvailable(true)
             powerInteractor.setAsleepForTest()
 
             // WHEN the primaryBouncer stops showing
@@ -1085,7 +982,8 @@
             // THEN a transition to AOD should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromPrimaryBouncerTransitionInteractor",
+                    ownerName =
+                        "FromPrimaryBouncerTransitionInteractor" + "(Sleep transition triggered)",
                     from = KeyguardState.PRIMARY_BOUNCER,
                     to = KeyguardState.AOD,
                     animatorAssertion = { it.isNotNull() },
@@ -1112,7 +1010,8 @@
             // THEN a transition to DOZING should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromPrimaryBouncerTransitionInteractor",
+                    ownerName =
+                        "FromPrimaryBouncerTransitionInteractor" + "(Sleep transition triggered)",
                     from = KeyguardState.PRIMARY_BOUNCER,
                     to = KeyguardState.DOZING,
                     animatorAssertion = { it.isNotNull() },
@@ -1642,7 +1541,9 @@
             // THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromLockscreenTransitionInteractor",
+                    ownerName =
+                        "FromLockscreenTransitionInteractor" +
+                            "(#listenForLockscreenToPrimaryBouncerDragging)",
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.PRIMARY_BOUNCER,
                     animatorAssertion = { it.isNull() }, // dragging should be manually animated
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index 2f92afa..83e4d31 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -84,7 +84,7 @@
 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.media.dialog.MediaOutputDialogManager
 import com.android.systemui.monet.ColorScheme
 import com.android.systemui.monet.Style
 import com.android.systemui.plugins.ActivityStarter
@@ -160,7 +160,7 @@
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var expandedSet: ConstraintSet
     @Mock private lateinit var collapsedSet: ConstraintSet
-    @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
+    @Mock private lateinit var mediaOutputDialogManager: MediaOutputDialogManager
     @Mock private lateinit var mediaCarouselController: MediaCarouselController
     @Mock private lateinit var falsingManager: FalsingManager
     @Mock private lateinit var transitionParent: ViewGroup
@@ -266,7 +266,7 @@
                     mediaViewController,
                     seekBarViewModel,
                     Lazy { mediaDataManager },
-                    mediaOutputDialogFactory,
+                    mediaOutputDialogManager,
                     mediaCarouselController,
                     falsingManager,
                     clock,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
index 0879884..83def8e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
@@ -42,16 +42,16 @@
 
     private MediaOutputDialogReceiver mMediaOutputDialogReceiver;
 
-    private final MediaOutputDialogFactory mMockMediaOutputDialogFactory =
-            mock(MediaOutputDialogFactory.class);
+    private final MediaOutputDialogManager mMockMediaOutputDialogManager =
+            mock(MediaOutputDialogManager.class);
 
-    private final MediaOutputBroadcastDialogFactory mMockMediaOutputBroadcastDialogFactory =
-            mock(MediaOutputBroadcastDialogFactory.class);
+    private final MediaOutputBroadcastDialogManager mMockMediaOutputBroadcastDialogManager =
+            mock(MediaOutputBroadcastDialogManager.class);
 
     @Before
     public void setup() {
-        mMediaOutputDialogReceiver = new MediaOutputDialogReceiver(mMockMediaOutputDialogFactory,
-                mMockMediaOutputBroadcastDialogFactory);
+        mMediaOutputDialogReceiver = new MediaOutputDialogReceiver(mMockMediaOutputDialogManager,
+                mMockMediaOutputBroadcastDialogManager);
     }
 
     @Test
@@ -60,9 +60,10 @@
         intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, times(1))
-                .create(getContext().getPackageName(), false, null);
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, times(1))
+                .createAndShow(getContext().getPackageName(), false, null);
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -71,8 +72,9 @@
         intent.putExtra("Wrong Package Name Key", getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -80,8 +82,9 @@
         Intent intent = new Intent(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG);
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -92,8 +95,9 @@
         intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -104,9 +108,9 @@
         intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, times(1))
-                .create(getContext().getPackageName(), true, null);
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, times(1))
+                .createAndShow(getContext().getPackageName(), true, null);
     }
 
     @Test
@@ -117,8 +121,9 @@
         intent.putExtra("Wrong Package Name Key", getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -128,8 +133,9 @@
                 MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG);
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -139,8 +145,9 @@
         intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 
     @Test
@@ -148,7 +155,8 @@
         Intent intent = new Intent("UnKnown Action");
         mMediaOutputDialogReceiver.onReceive(getContext(), intent);
 
-        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
-        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputDialogManager, never()).createAndShow(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogManager, never())
+                .createAndShow(any(), anyBoolean(), any());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
index dbfab64..bda0e1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
@@ -21,32 +21,25 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
+import com.android.systemui.mediaprojection.taskswitcher.activityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() {
 
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
-
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-
-    private val repo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
+    private val kosmos = taskSwitcherKosmos()
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val testScope = kosmos.testScope
+    private val repo = kosmos.activityTaskManagerTasksRepository
 
     @Test
     fun launchRecentTask_taskIsMovedToForeground() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
index fdd434a..6043ede 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -17,50 +17,35 @@
 package com.android.systemui.mediaprojection.taskswitcher.data.repository
 
 import android.os.Binder
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import android.view.ContentRecordingSession
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
 import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.mediaProjectionManagerRepository
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class MediaProjectionManagerRepositoryTest : SysuiTestCase() {
 
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
+    private val kosmos = taskSwitcherKosmos()
+    private val testScope = kosmos.testScope
 
-    private val fakeMediaProjectionManager = FakeMediaProjectionManager()
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
+    private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
 
-    private val tasksRepo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
-
-    private val repo =
-        MediaProjectionManagerRepository(
-            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
-            handler = Handler.getMain(),
-            applicationScope = testScope.backgroundScope,
-            tasksRepository = tasksRepo,
-            backgroundDispatcher = dispatcher,
-            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper
-        )
+    private val repo = kosmos.mediaProjectionManagerRepository
 
     @Test
     fun switchProjectedTask_stateIsUpdatedWithNewTask() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
index dfb688b..33e65f26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
@@ -17,55 +17,33 @@
 package com.android.systemui.mediaprojection.taskswitcher.domain.interactor
 
 import android.content.Intent
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createSingleTaskSession
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createSingleTaskSession
 import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherInteractor
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class TaskSwitchInteractorTest : SysuiTestCase() {
 
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
-    private val fakeMediaProjectionManager = FakeMediaProjectionManager()
-
-    private val tasksRepo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
-
-    private val mediaRepo =
-        MediaProjectionManagerRepository(
-            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
-            handler = Handler.getMain(),
-            applicationScope = testScope.backgroundScope,
-            tasksRepository = tasksRepo,
-            backgroundDispatcher = dispatcher,
-            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
-        )
-
-    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
+    private val kosmos = taskSwitcherKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
+    private val interactor = kosmos.taskSwitcherInteractor
 
     @Test
     fun taskSwitchChanges_notProjecting_foregroundTaskChange_emitsNotProjectingTask() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
index c4e9393..9382c58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
@@ -18,26 +18,22 @@
 
 import android.app.Notification
 import android.app.NotificationManager
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
-import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
-import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherViewModel
 import com.android.systemui.res.R
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -46,39 +42,16 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() {
 
     private val notificationManager = mock<NotificationManager>()
-
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
-    private val fakeMediaProjectionManager = FakeMediaProjectionManager()
-
-    private val tasksRepo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
-
-    private val mediaRepo =
-        MediaProjectionManagerRepository(
-            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
-            handler = Handler.getMain(),
-            applicationScope = testScope.backgroundScope,
-            tasksRepository = tasksRepo,
-            backgroundDispatcher = dispatcher,
-            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
-        )
-
-    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
-    private val viewModel =
-        TaskSwitcherNotificationViewModel(interactor, backgroundDispatcher = dispatcher)
+    private val kosmos = taskSwitcherKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
+    private val viewModel = kosmos.taskSwitcherViewModel
 
     private lateinit var coordinator: TaskSwitcherNotificationCoordinator
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
index 687f970..a468953 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
@@ -17,64 +17,35 @@
 package com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel
 
 import android.content.Intent
-import android.os.Handler
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createDisplaySession
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createSingleTaskSession
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
-import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createSingleTaskSession
+import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
+import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherViewModel
 import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState
 import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel.Companion.NOTIFICATION_MAX_SHOW_DURATION
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.milliseconds
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScheduler
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class TaskSwitcherNotificationViewModelTest : SysuiTestCase() {
 
-    private val scheduler = TestCoroutineScheduler()
-    private val dispatcher = UnconfinedTestDispatcher(scheduler)
-    private val testScope = TestScope(dispatcher)
-
-    private val fakeActivityTaskManager = FakeActivityTaskManager()
-    private val fakeMediaProjectionManager = FakeMediaProjectionManager()
-
-    private val tasksRepo =
-        ActivityTaskManagerTasksRepository(
-            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
-            applicationScope = testScope.backgroundScope,
-            backgroundDispatcher = dispatcher
-        )
-
-    private val mediaRepo =
-        MediaProjectionManagerRepository(
-            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
-            handler = Handler.getMain(),
-            applicationScope = testScope.backgroundScope,
-            tasksRepository = tasksRepo,
-            backgroundDispatcher = dispatcher,
-            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
-        )
-
-    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
-
-    private val viewModel =
-        TaskSwitcherNotificationViewModel(interactor, backgroundDispatcher = dispatcher)
+    private val kosmos = taskSwitcherKosmos()
+    private val testScope = kosmos.testScope
+    private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
+    private val viewModel = kosmos.taskSwitcherViewModel
 
     @Test
     fun uiState_notProjecting_emitsNotShowing() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
index a59ba07..eb692eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java
@@ -32,9 +32,9 @@
 import android.view.LayoutInflater;
 import android.view.View;
 
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -51,6 +51,7 @@
 import java.util.List;
 import java.util.Map;
 
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -65,7 +66,7 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mDependency.injectMockDependency(MediaOutputDialogFactory.class);
+        mDependency.injectMockDependency(MediaOutputDialogManager.class);
         allowTestableLooperAsMainThread();
         when(mBindStage.getStageParams(any())).thenReturn(new RowContentBindParams());
         mDynamicChildBindController =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
index b3fc25c..24195fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
+import android.platform.test.annotations.DisableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
 import com.android.systemui.SysUITestComponent
 import com.android.systemui.SysUITestModule
@@ -238,6 +240,7 @@
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun animationsEnabled_isTrue_whenKeyguardIsShowing() =
         testComponent.runTest {
             keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index fb49499f..718f998 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -58,7 +58,7 @@
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -159,7 +159,7 @@
         dependency.injectTestDependency(FeatureFlags.class, mFeatureFlags);
         dependency.injectMockDependency(NotificationMediaManager.class);
         dependency.injectMockDependency(NotificationShadeWindowController.class);
-        dependency.injectMockDependency(MediaOutputDialogFactory.class);
+        dependency.injectMockDependency(MediaOutputDialogManager.class);
         mMockLogger = mock(ExpandableNotificationRowLogger.class);
         mStatusBarStateController = mock(StatusBarStateController.class);
         mKeyguardBypassController = mock(KeyguardBypassController.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt
new file mode 100644
index 0000000..f88bd7e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.platform.test.annotations.EnableFlags
+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.keyguard.shared.model.StatusBarState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationViewFlipperPausing.FLAG_NAME)
+class NotificationViewFlipperViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    val underTest
+        get() = kosmos.notificationViewFlipperViewModel
+
+    @Test
+    fun testIsPaused_falseWhenViewingShade() =
+        kosmos.testScope.runTest {
+            val isPaused by collectLastValue(underTest.isPaused)
+
+            // WHEN shade is open
+            kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            runCurrent()
+
+            // THEN view flippers should NOT be paused
+            assertThat(isPaused).isFalse()
+        }
+
+    @Test
+    fun testIsPaused_trueWhenViewingKeyguard() =
+        kosmos.testScope.runTest {
+            val isPaused by collectLastValue(underTest.isPaused)
+
+            // WHEN on keyguard
+            kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            runCurrent()
+
+            // THEN view flippers should be paused
+            assertThat(isPaused).isTrue()
+        }
+
+    @Test
+    fun testIsPaused_trueWhenStartingToSleep() =
+        kosmos.testScope.runTest {
+            val isPaused by collectLastValue(underTest.isPaused)
+
+            // WHEN shade is open
+            kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            // AND device is starting to go to sleep
+            kosmos.fakePowerRepository.updateWakefulness(WakefulnessState.STARTING_TO_SLEEP)
+            runCurrent()
+
+            // THEN view flippers should be paused
+            assertThat(isPaused).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 3d75288..4715b33 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -57,39 +57,6 @@
             )
     }
 
-    // region isDimmed
-    @Test
-    fun isDimmed_whenTrue_shouldReturnTrue() {
-        sut.arrangeDimmed(true)
-
-        assertThat(sut.isDimmed).isTrue()
-    }
-
-    @Test
-    fun isDimmed_whenFalse_shouldReturnFalse() {
-        sut.arrangeDimmed(false)
-
-        assertThat(sut.isDimmed).isFalse()
-    }
-
-    @Test
-    fun isDimmed_whenDozeAmountIsEmpty_shouldReturnTrue() {
-        sut.arrangeDimmed(true)
-        sut.dozeAmount = 0f
-
-        assertThat(sut.isDimmed).isTrue()
-    }
-
-    @Test
-    fun isDimmed_whenPulseExpandingIsFalse_shouldReturnTrue() {
-        sut.arrangeDimmed(true)
-        sut.arrangePulseExpanding(false)
-        sut.dozeAmount = 1f // arrangePulseExpanding changes dozeAmount
-
-        assertThat(sut.isDimmed).isTrue()
-    }
-    // endregion
-
     // region pulseHeight
     @Test
     fun pulseHeight_whenValueChanged_shouldCallListener() {
@@ -383,12 +350,6 @@
 }
 
 // region Arrange helper methods.
-private fun AmbientState.arrangeDimmed(value: Boolean) {
-    isDimmed = value
-    dozeAmount = if (value) 0f else 1f
-    arrangePulseExpanding(!value)
-}
-
 private fun AmbientState.arrangePulseExpanding(value: Boolean) {
     if (value) {
         dozeAmount = 1f
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index f326cea..28dfbbb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -307,14 +307,6 @@
     }
 
     @Test
-    public void testNotDimmedOnKeyguard() {
-        when(mBarState.getState()).thenReturn(StatusBarState.SHADE);
-        mStackScroller.setDimmed(true /* dimmed */, false /* animate */);
-        mStackScroller.setDimmed(true /* dimmed */, true /* animate */);
-        assertFalse(mStackScroller.isDimmed());
-    }
-
-    @Test
     public void updateEmptyView_dndSuppressing() {
         when(mEmptyShadeView.willBeGone()).thenReturn(true);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index f050857..562aa6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -95,6 +95,7 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.domain.interactor.StatusBarKeyguardViewManagerInteractor;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -226,7 +227,8 @@
                         mSelectedUserInteractor,
                         () -> mock(KeyguardSurfaceBehindInteractor.class),
                         mock(JavaAdapter.class),
-                        () -> mock(SceneInteractor.class)) {
+                        () -> mock(SceneInteractor.class),
+                        mock(StatusBarKeyguardViewManagerInteractor.class)) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
@@ -736,7 +738,8 @@
                         mSelectedUserInteractor,
                         () -> mock(KeyguardSurfaceBehindInteractor.class),
                         mock(JavaAdapter.class),
-                        () -> mock(SceneInteractor.class)) {
+                        () -> mock(SceneInteractor.class),
+                        mock(StatusBarKeyguardViewManagerInteractor.class)) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 3a6324d..d0261ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -69,7 +69,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.media.dialog.MediaOutputDialogManager;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.plugins.VolumeDialogController.State;
 import com.android.systemui.res.R;
@@ -130,7 +130,7 @@
     @Mock
     DeviceProvisionedController mDeviceProvisionedController;
     @Mock
-    MediaOutputDialogFactory mMediaOutputDialogFactory;
+    MediaOutputDialogManager mMediaOutputDialogManager;
     @Mock
     InteractionJankMonitor mInteractionJankMonitor;
     @Mock
@@ -196,7 +196,7 @@
                 mAccessibilityMgr,
                 mDeviceProvisionedController,
                 mConfigurationController,
-                mMediaOutputDialogFactory,
+                mMediaOutputDialogManager,
                 mInteractionJankMonitor,
                 mVolumePanelNavigationInteractor,
                 mVolumeNavigator,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt
index c3f677e..d5411ad 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.activityStarter
@@ -40,5 +41,6 @@
             context = mockedContext,
             activityStarter = activityStarter,
             powerInteractor = powerInteractor,
+            keyguardTransitionInteractor = keyguardTransitionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index a9a2d91..dcbd577 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.annotation.FloatRange
+import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -65,55 +66,79 @@
     }
 
     /**
-     * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling
-     * [runCurrent] after each step.
+     * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step.
+     *
+     * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part
+     * way using [throughTransitionState].
      */
     suspend fun sendTransitionSteps(
         from: KeyguardState,
         to: KeyguardState,
         testScope: TestScope,
+        throughTransitionState: TransitionState = TransitionState.FINISHED,
     ) {
-        sendTransitionSteps(from, to, testScope.testScheduler)
+        sendTransitionSteps(from, to, testScope.testScheduler, throughTransitionState)
     }
 
     /**
-     * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling
-     * [runCurrent] after each step.
+     * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step.
+     *
+     * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part
+     * way using [throughTransitionState].
      */
     suspend fun sendTransitionSteps(
         from: KeyguardState,
         to: KeyguardState,
         testScheduler: TestCoroutineScheduler,
+        throughTransitionState: TransitionState = TransitionState.FINISHED,
     ) {
         sendTransitionStep(
-            TransitionStep(
-                transitionState = TransitionState.STARTED,
-                from = from,
-                to = to,
-                value = 0f,
-            )
+            step =
+                TransitionStep(
+                    transitionState = TransitionState.STARTED,
+                    from = from,
+                    to = to,
+                    value = 0f,
+                )
         )
         testScheduler.runCurrent()
 
-        sendTransitionStep(
-            TransitionStep(
-                transitionState = TransitionState.RUNNING,
-                from = from,
-                to = to,
-                value = 0.5f
+        if (
+            throughTransitionState == TransitionState.RUNNING ||
+                throughTransitionState == TransitionState.FINISHED
+        ) {
+            sendTransitionStep(
+                step =
+                    TransitionStep(
+                        transitionState = TransitionState.RUNNING,
+                        from = from,
+                        to = to,
+                        value = 0.5f
+                    )
             )
-        )
-        testScheduler.runCurrent()
+            testScheduler.runCurrent()
+        }
 
-        sendTransitionStep(
-            TransitionStep(
-                transitionState = TransitionState.FINISHED,
-                from = from,
-                to = to,
-                value = 1f,
+        if (throughTransitionState == TransitionState.FINISHED) {
+            sendTransitionStep(
+                step =
+                    TransitionStep(
+                        transitionState = TransitionState.FINISHED,
+                        from = from,
+                        to = to,
+                        value = 1f,
+                    )
             )
+            testScheduler.runCurrent()
+        }
+    }
+
+    suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) {
+        this.sendTransitionStep(
+            step = step,
+            validateStep = validateStep,
+            ownerName = step.ownerName
         )
-        testScheduler.runCurrent()
     }
 
     /**
@@ -132,7 +157,22 @@
      * If you're testing something involving transitions themselves and are sure you want to send
      * only a FINISHED step, override [validateStep].
      */
-    suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) {
+    suspend fun sendTransitionStep(
+        from: KeyguardState = KeyguardState.OFF,
+        to: KeyguardState = KeyguardState.OFF,
+        value: Float = 0f,
+        transitionState: TransitionState = TransitionState.FINISHED,
+        ownerName: String = "",
+        step: TransitionStep =
+            TransitionStep(
+                from = from,
+                to = to,
+                value = value,
+                transitionState = transitionState,
+                ownerName = ownerName
+            ),
+        validateStep: Boolean = true
+    ) {
         _transitions.replayCache.last().let { lastStep ->
             if (
                 validateStep &&
@@ -159,7 +199,9 @@
         step: TransitionStep,
         validateStep: Boolean = true
     ): Job {
-        return coroutineScope.launch { sendTransitionStep(step, validateStep) }
+        return coroutineScope.launch {
+            sendTransitionStep(step = step, validateStep = validateStep)
+        }
     }
 
     suspend fun sendTransitionSteps(
@@ -168,12 +210,13 @@
         validateStep: Boolean = true
     ) {
         steps.forEach {
-            sendTransitionStep(it, validateStep = validateStep)
+            sendTransitionStep(step = it, validateStep = validateStep)
             testScope.testScheduler.runCurrent()
         }
     }
 
     override fun startTransition(info: TransitionInfo): UUID? {
+        Log.i("TEST", "Start transition: ", Exception())
         return if (info.animator == null) UUID.randomUUID() else null
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt
new file mode 100644
index 0000000..4c8bf90
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.keyguardOcclusionRepository by Kosmos.Fixture { KeyguardOcclusionRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..530cbed
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.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 com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+val Kosmos.fromAlternateBouncerTransitionInteractor by
+    Kosmos.Fixture {
+        FromAlternateBouncerTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            communalInteractor = communalInteractor,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
index 2477415..bbe37c1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
@@ -18,19 +18,21 @@
 
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 
 val Kosmos.fromAodTransitionInteractor by
     Kosmos.Fixture {
         FromAodTransitionInteractor(
             transitionRepository = fakeKeyguardTransitionRepository,
             transitionInteractor = keyguardTransitionInteractor,
-            scope = testScope,
+            scope = applicationCoroutineScope,
             bgDispatcher = testDispatcher,
             mainDispatcher = testDispatcher,
             keyguardInteractor = keyguardInteractor,
             powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..23dcd96
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.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 com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+var Kosmos.fromDozingTransitionInteractor by
+    Kosmos.Fixture {
+        FromDozingTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            communalInteractor = communalInteractor,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..f7a9d59
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.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.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+var Kosmos.fromDreamingLockscreenHostedTransitionInteractor by
+    Kosmos.Fixture {
+        FromDreamingLockscreenHostedTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..135644c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.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.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+var Kosmos.fromDreamingTransitionInteractor by
+    Kosmos.Fixture {
+        FromDreamingTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            glanceableHubTransitions = glanceableHubTransitions,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..1695327
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.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.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+var Kosmos.fromGlanceableHubTransitionInteractor by
+    Kosmos.Fixture {
+        FromGlanceableHubTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+            glanceableHubTransitions = glanceableHubTransitions,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
index 25fc67a..604d9e4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
@@ -17,11 +17,13 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 
 val Kosmos.fromGoneTransitionInteractor by
     Kosmos.Fixture {
@@ -34,5 +36,7 @@
             keyguardInteractor = keyguardInteractor,
             powerInteractor = powerInteractor,
             communalInteractor = communalInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+            biometricSettingsRepository = biometricSettingsRepository,
         )
     }
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 3b52676..162fd90 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
@@ -23,6 +23,7 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 
 var Kosmos.fromLockscreenTransitionInteractor by
     Kosmos.Fixture {
@@ -38,5 +39,6 @@
             powerInteractor = powerInteractor,
             glanceableHubTransitions = glanceableHubTransitions,
             swipeToDismissInteractor = swipeToDismissInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..fc740a1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.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 com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+val Kosmos.fromOccludedTransitionInteractor by
+    Kosmos.Fixture {
+        FromOccludedTransitionInteractor(
+            transitionRepository = keyguardTransitionRepository,
+            transitionInteractor = keyguardTransitionInteractor,
+            scope = applicationCoroutineScope,
+            bgDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
+            keyguardInteractor = keyguardInteractor,
+            powerInteractor = powerInteractor,
+            communalInteractor = communalInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
index 6b76449..98babff 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
 
 var Kosmos.fromPrimaryBouncerTransitionInteractor by
@@ -40,5 +41,6 @@
             keyguardSecurityModel = keyguardSecurityModel,
             selectedUserInteractor = selectedUserInteractor,
             powerInteractor = powerInteractor,
+            keyguardOcclusionInteractor = keyguardOcclusionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
index 6df7493..6cc1e8e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
@@ -16,19 +16,21 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
-import dagger.Lazy
 
 val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by
     Kosmos.Fixture {
         KeyguardTransitionInteractor(
             scope = applicationCoroutineScope,
             repository = keyguardTransitionRepository,
-            fromLockscreenTransitionInteractor = Lazy { fromLockscreenTransitionInteractor },
-            fromPrimaryBouncerTransitionInteractor =
-                Lazy { fromPrimaryBouncerTransitionInteractor },
-            fromAodTransitionInteractor = Lazy { fromAodTransitionInteractor },
+            keyguardRepository = keyguardRepository,
+            fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor },
+            fromPrimaryBouncerTransitionInteractor = { fromPrimaryBouncerTransitionInteractor },
+            fromAodTransitionInteractor = { fromAodTransitionInteractor },
+            fromAlternateBouncerTransitionInteractor = { fromAlternateBouncerTransitionInteractor },
+            fromDozingTransitionInteractor = { fromDozingTransitionInteractor },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..8162520
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.dozingToOccludedTransitionViewModel by
+    Kosmos.Fixture {
+        DozingToOccludedTransitionViewModel(
+            animationFlow = keyguardTransitionAnimationFlow,
+        )
+    }
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 c2300a1e..75e3ac2 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
@@ -13,7 +13,6 @@
  * 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
@@ -41,8 +40,10 @@
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
         aodToGoneTransitionViewModel = aodToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
+        aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
         dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel,
         dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
+        dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
         dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
         glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
         goneToAodTransitionViewModel = goneToAodTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/MediaKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/MediaKosmos.kt
index e1b1966..e788669 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/MediaKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/MediaKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.media
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.media.dialog.MediaOutputDialogManager
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.mediaOutputDialogFactory: MediaOutputDialogFactory by Kosmos.Fixture { mock {} }
+var Kosmos.mediaOutputDialogManager: MediaOutputDialogManager by Kosmos.Fixture { mock {} }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeActivityTaskManager.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeActivityTaskManager.kt
index 920e5ee..41d2d60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeActivityTaskManager.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,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.mediaprojection.taskswitcher.data.repository
+package com.android.systemui.mediaprojection.taskswitcher
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.app.IActivityTaskManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt
index 28393e8..2b6032c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.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,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.mediaprojection.taskswitcher.data.repository
+package com.android.systemui.mediaprojection.taskswitcher
 
 import android.media.projection.MediaProjectionInfo
 import android.media.projection.MediaProjectionManager
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt
new file mode 100644
index 0000000..d344b75
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.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.mediaprojection.taskswitcher
+
+import android.os.Handler
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
+import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+
+val Kosmos.fakeActivityTaskManager by Kosmos.Fixture { FakeActivityTaskManager() }
+
+val Kosmos.fakeMediaProjectionManager by Kosmos.Fixture { FakeMediaProjectionManager() }
+
+val Kosmos.activityTaskManagerTasksRepository by
+    Kosmos.Fixture {
+        ActivityTaskManagerTasksRepository(
+            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
+            applicationScope = applicationCoroutineScope,
+            backgroundDispatcher = testDispatcher
+        )
+    }
+
+val Kosmos.mediaProjectionManagerRepository by
+    Kosmos.Fixture {
+        MediaProjectionManagerRepository(
+            mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
+            handler = Handler.getMain(),
+            applicationScope = applicationCoroutineScope,
+            tasksRepository = activityTaskManagerTasksRepository,
+            backgroundDispatcher = testDispatcher,
+            mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
+        )
+    }
+
+val Kosmos.taskSwitcherInteractor by
+    Kosmos.Fixture {
+        TaskSwitchInteractor(mediaProjectionManagerRepository, activityTaskManagerTasksRepository)
+    }
+
+val Kosmos.taskSwitcherViewModel by
+    Kosmos.Fixture { TaskSwitcherNotificationViewModel(taskSwitcherInteractor, testDispatcher) }
+
+@OptIn(ExperimentalCoroutinesApi::class)
+fun taskSwitcherKosmos() = Kosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/battery/BatterySaverTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/battery/BatterySaverTileKosmos.kt
new file mode 100644
index 0000000..a2d1d93
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/battery/BatterySaverTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.battery
+
+import com.android.systemui.battery.BatterySaverModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+
+val Kosmos.qsBatterySaverTileConfig by
+    Kosmos.Fixture { BatterySaverModule.provideBatterySaverTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt
new file mode 100644
index 0000000..6772ba3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.internet
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.connectivity.ConnectivityModule
+
+val Kosmos.qsInternetTileConfig by
+    Kosmos.Fixture { ConnectivityModule.provideInternetTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt
new file mode 100644
index 0000000..d793740
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.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.statusbar.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.keyguardOcclusionInteractor by
+    Kosmos.Fixture {
+        KeyguardOcclusionInteractor(
+            scope = testScope.backgroundScope,
+            repository = keyguardOcclusionRepository,
+            powerInteractor = powerInteractor,
+            transitionInteractor = keyguardTransitionInteractor,
+            keyguardInteractor = keyguardInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt
new file mode 100644
index 0000000..9e34fe8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.domain.interactor
+
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.statusBarKeyguardViewManagerInteractor by
+    Kosmos.Fixture {
+        StatusBarKeyguardViewManagerInteractor(
+            keyguardTransitionInteractor = this.keyguardTransitionInteractor,
+            keyguardOcclusionInteractor = this.keyguardOcclusionInteractor,
+            powerInteractor = this.powerInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt
new file mode 100644
index 0000000..7ffa262
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
+
+val Kosmos.notificationViewFlipperViewModel by Fixture {
+    NotificationViewFlipperViewModel(
+        dumpManager = dumpManager,
+        stackInteractor = notificationStackInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 106e85c..c013664 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -23,7 +23,9 @@
 import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.goneToAodTransitionViewModel
@@ -57,7 +59,9 @@
         communalInteractor = communalInteractor,
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
+        aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
         dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
+        dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
         dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
         goneToAodTransitionViewModel = goneToAodTransitionViewModel,
         goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
index 5ae033c..d798b3b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
@@ -30,6 +30,8 @@
     private boolean mIsAodPowerSave = false;
     private boolean mWirelessCharging;
     private boolean mPowerSaveMode = false;
+    private boolean mIsPluggedIn = false;
+    private boolean mIsExtremePowerSave = false;
 
     private final List<BatteryStateChangeCallback> mCallbacks = new ArrayList<>();
 
@@ -64,8 +66,35 @@
     }
 
     @Override
+    public boolean isExtremeSaverOn() {
+        return mIsExtremePowerSave;
+    }
+
+    /**
+     * Note: this does not affect the regular power saver. Triggers all callbacks, only on change.
+     */
+    public void setExtremeSaverOn(Boolean extremePowerSave) {
+        if (extremePowerSave == mIsExtremePowerSave) return;
+
+        mIsExtremePowerSave = extremePowerSave;
+        for (BatteryStateChangeCallback callback: mCallbacks) {
+            callback.onExtremeBatterySaverChanged(extremePowerSave);
+        }
+    }
+
+    @Override
     public boolean isPluggedIn() {
-        return false;
+        return mIsPluggedIn;
+    }
+
+    /**
+     * Notifies all registered callbacks
+     */
+    public void setPluggedIn(boolean pluggedIn) {
+        mIsPluggedIn = pluggedIn;
+        for (BatteryStateChangeCallback cb : mCallbacks) {
+            cb.onBatteryLevelChanged(0, pluggedIn, false);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index a3b1a0e..3938f77 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.mediaOutputDialogFactory
+import com.android.systemui.media.mediaOutputDialogManager
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -42,7 +42,7 @@
     Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } }
 
 val Kosmos.mediaOutputActionsInteractor by
-    Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogFactory) }
+    Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogManager) }
 val Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() }
 val Kosmos.mediaOutputInteractor by
     Kosmos.Fixture {
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 6d731b2..b1672ed 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -210,7 +210,7 @@
                     mActivityListener.onTopActivityChanged(displayId, topActivity,
                             UserHandle.USER_NULL);
                 } catch (RemoteException e) {
-                    Slog.w(TAG, "Unable to call mActivityListener", e);
+                    Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
                 }
             }
 
@@ -220,7 +220,7 @@
                 try {
                     mActivityListener.onTopActivityChanged(displayId, topActivity, userId);
                 } catch (RemoteException e) {
-                    Slog.w(TAG, "Unable to call mActivityListener", e);
+                    Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
                 }
             }
 
@@ -229,7 +229,7 @@
                 try {
                     mActivityListener.onDisplayEmpty(displayId);
                 } catch (RemoteException e) {
-                    Slog.w(TAG, "Unable to call mActivityListener", e);
+                    Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
                 }
             }
         };
@@ -1213,7 +1213,7 @@
         mContext.startActivityAsUser(
                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK),
                 ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(),
-                mContext.getUser());
+                UserHandle.SYSTEM);
     }
 
     private void onSecureWindowShown(int displayId, int uid) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 663ba8a..cfe1e18 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4779,36 +4779,47 @@
                 // being bound to an application.
                 thread.runIsolatedEntryPoint(
                         app.getIsolatedEntryPoint(), app.getIsolatedEntryPointArgs());
-            } else if (instr2 != null) {
-                thread.bindApplication(processName, appInfo,
-                        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
-                        instr2.mIsSdkInSandbox,
-                        providerList,
-                        instr2.mClass,
-                        profilerInfo, instr2.mArguments,
-                        instr2.mWatcher,
-                        instr2.mUiAutomationConnection, testMode,
-                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
-                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
-                        new Configuration(app.getWindowProcessController().getConfiguration()),
-                        app.getCompat(), getCommonServicesLocked(app.isolated),
-                        mCoreSettingsObserver.getCoreSettingsLocked(),
-                        buildSerial, autofillOptions, contentCaptureOptions,
-                        app.getDisabledCompatChanges(), serializedSystemFontMap,
-                        app.getStartElapsedTime(), app.getStartUptime());
             } else {
-                thread.bindApplication(processName, appInfo,
-                        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
-                        /* isSdkInSandbox= */ false,
-                        providerList, null, profilerInfo, null, null, null, testMode,
-                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
-                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
+                boolean isSdkInSandbox = false;
+                ComponentName instrumentationName = null;
+                Bundle instrumentationArgs = null;
+                IInstrumentationWatcher instrumentationWatcher = null;
+                IUiAutomationConnection instrumentationUiConnection = null;
+                if (instr2 != null) {
+                    isSdkInSandbox = instr2.mIsSdkInSandbox;
+                    instrumentationName = instr2.mClass;
+                    instrumentationArgs = instr2.mArguments;
+                    instrumentationWatcher = instr2.mWatcher;
+                    instrumentationUiConnection = instr2.mUiAutomationConnection;
+                }
+                thread.bindApplication(
+                        processName,
+                        appInfo,
+                        app.sdkSandboxClientAppVolumeUuid,
+                        app.sdkSandboxClientAppPackage,
+                        isSdkInSandbox,
+                        providerList,
+                        instrumentationName,
+                        profilerInfo,
+                        instrumentationArgs,
+                        instrumentationWatcher,
+                        instrumentationUiConnection,
+                        testMode,
+                        mBinderTransactionTrackingEnabled,
+                        enableTrackAllocation,
+                        isRestrictedBackupMode || !normalMode,
+                        app.isPersistent(),
                         new Configuration(app.getWindowProcessController().getConfiguration()),
-                        app.getCompat(), getCommonServicesLocked(app.isolated),
+                        app.getCompat(),
+                        getCommonServicesLocked(app.isolated),
                         mCoreSettingsObserver.getCoreSettingsLocked(),
-                        buildSerial, autofillOptions, contentCaptureOptions,
-                        app.getDisabledCompatChanges(), serializedSystemFontMap,
-                        app.getStartElapsedTime(), app.getStartUptime());
+                        buildSerial,
+                        autofillOptions,
+                        contentCaptureOptions,
+                        app.getDisabledCompatChanges(),
+                        serializedSystemFontMap,
+                        app.getStartElapsedTime(),
+                        app.getStartUptime());
             }
 
             Message msg = mHandler.obtainMessage(BIND_APPLICATION_TIMEOUT_SOFT_MSG);
@@ -9363,7 +9374,9 @@
                 sb.append("Animations-Running: ").append(info.numAnimationsRunning).append("\n");
             }
             if (info.broadcastIntentAction != null) {
-                sb.append("Broadcast-Intent-Action: ").append(info.broadcastIntentAction).append("\n");
+                sb.append("Broadcast-Intent-Action: ")
+                        .append(info.broadcastIntentAction)
+                        .append("\n");
             }
             if (info.durationMillis != -1) {
                 sb.append("Duration-Millis: ").append(info.durationMillis).append("\n");
@@ -9723,82 +9736,126 @@
         // If process is null, we are being called from some internal code
         // and may be about to die -- run this synchronously.
         final boolean runSynchronously = process == null;
-        Thread worker = new Thread("Error dump: " + dropboxTag) {
-            @Override
-            public void run() {
-                if (report != null) {
-                    sb.append(report);
-                }
-
-                String logcatSetting = Settings.Global.ERROR_LOGCAT_PREFIX + dropboxTag;
-                String maxBytesSetting = Settings.Global.MAX_ERROR_BYTES_PREFIX + dropboxTag;
-                int lines = Build.IS_USER
-                        ? 0
-                        : Settings.Global.getInt(mContext.getContentResolver(), logcatSetting, 0);
-                int dropboxMaxSize = Settings.Global.getInt(
-                        mContext.getContentResolver(), maxBytesSetting, DROPBOX_DEFAULT_MAX_SIZE);
-
-                if (dataFile != null) {
-                    // Attach the stack traces file to the report so collectors can load them
-                    // by file if they have access.
-                    sb.append(DATA_FILE_PATH_HEADER)
-                            .append(dataFile.getAbsolutePath()).append('\n');
-
-                    int maxDataFileSize = dropboxMaxSize
-                            - sb.length()
-                            - lines * RESERVED_BYTES_PER_LOGCAT_LINE
-                            - DATA_FILE_PATH_FOOTER.length();
-
-                    if (maxDataFileSize > 0) {
-                        // Inline dataFile contents if there is room.
-                        try {
-                            sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
-                                    "\n\n[[TRUNCATED]]\n"));
-                        } catch (IOException e) {
-                            Slog.e(TAG, "Error reading " + dataFile, e);
+        Thread worker =
+                new Thread("Error dump: " + dropboxTag) {
+                    @Override
+                    public void run() {
+                        if (report != null) {
+                            sb.append(report);
                         }
+
+                        String logcatSetting = Settings.Global.ERROR_LOGCAT_PREFIX + dropboxTag;
+                        String maxBytesSetting =
+                                Settings.Global.MAX_ERROR_BYTES_PREFIX + dropboxTag;
+                        int lines =
+                                Build.IS_USER
+                                        ? 0
+                                        : Settings.Global.getInt(
+                                                mContext.getContentResolver(), logcatSetting, 0);
+                        int dropboxMaxSize =
+                                Settings.Global.getInt(
+                                        mContext.getContentResolver(),
+                                        maxBytesSetting,
+                                        DROPBOX_DEFAULT_MAX_SIZE);
+
+                        if (dataFile != null) {
+                            // Attach the stack traces file to the report so collectors can load
+                            // them
+                            // by file if they have access.
+                            sb.append(DATA_FILE_PATH_HEADER)
+                                    .append(dataFile.getAbsolutePath())
+                                    .append('\n');
+
+                            int maxDataFileSize =
+                                    dropboxMaxSize
+                                            - sb.length()
+                                            - lines * RESERVED_BYTES_PER_LOGCAT_LINE
+                                            - DATA_FILE_PATH_FOOTER.length();
+
+                            if (maxDataFileSize > 0) {
+                                // Inline dataFile contents if there is room.
+                                try {
+                                    sb.append(
+                                            FileUtils.readTextFile(
+                                                    dataFile,
+                                                    maxDataFileSize,
+                                                    "\n\n[[TRUNCATED]]\n"));
+                                } catch (IOException e) {
+                                    Slog.e(TAG, "Error reading " + dataFile, e);
+                                }
+                            }
+
+                            // Always append the footer, even there wasn't enough space to inline
+                            // the
+                            // dataFile contents.
+                            sb.append(DATA_FILE_PATH_FOOTER);
+                        }
+
+                        if (crashInfo != null && crashInfo.stackTrace != null) {
+                            sb.append(crashInfo.stackTrace);
+                        }
+
+                        if (lines > 0 && !runSynchronously) {
+                            sb.append("\n");
+
+                            InputStreamReader input = null;
+                            try {
+                                java.lang.Process logcat =
+                                        new ProcessBuilder(
+                                                        // Time out after 10s of inactivity, but
+                                                        // kill logcat with SEGV
+                                                        // so we can investigate why it didn't
+                                                        // finish.
+                                                        "/system/bin/timeout",
+                                                        "-i",
+                                                        "-s",
+                                                        "SEGV",
+                                                        "10s",
+                                                        // Merge several logcat streams, and take
+                                                        // the last N lines.
+                                                        "/system/bin/logcat",
+                                                        "-v",
+                                                        "threadtime",
+                                                        "-b",
+                                                        "events",
+                                                        "-b",
+                                                        "system",
+                                                        "-b",
+                                                        "main",
+                                                        "-b",
+                                                        "crash",
+                                                        "-t",
+                                                        String.valueOf(lines))
+                                                .redirectErrorStream(true)
+                                                .start();
+
+                                try {
+                                    logcat.getOutputStream().close();
+                                } catch (IOException e) {
+                                }
+                                try {
+                                    logcat.getErrorStream().close();
+                                } catch (IOException e) {
+                                }
+                                input = new InputStreamReader(logcat.getInputStream());
+
+                                int num;
+                                char[] buf = new char[8192];
+                                while ((num = input.read(buf)) > 0) sb.append(buf, 0, num);
+                            } catch (IOException e) {
+                                Slog.e(TAG, "Error running logcat", e);
+                            } finally {
+                                if (input != null)
+                                    try {
+                                        input.close();
+                                    } catch (IOException e) {
+                                    }
+                            }
+                        }
+
+                        dbox.addText(dropboxTag, sb.toString());
                     }
-
-                    // Always append the footer, even there wasn't enough space to inline the
-                    // dataFile contents.
-                    sb.append(DATA_FILE_PATH_FOOTER);
-                }
-
-                if (crashInfo != null && crashInfo.stackTrace != null) {
-                    sb.append(crashInfo.stackTrace);
-                }
-
-                if (lines > 0 && !runSynchronously) {
-                    sb.append("\n");
-
-                    InputStreamReader input = null;
-                    try {
-                        java.lang.Process logcat = new ProcessBuilder(
-                                // Time out after 10s of inactivity, but kill logcat with SEGV
-                                // so we can investigate why it didn't finish.
-                                "/system/bin/timeout", "-i", "-s", "SEGV", "10s",
-                                // Merge several logcat streams, and take the last N lines.
-                                "/system/bin/logcat", "-v", "threadtime", "-b", "events", "-b", "system",
-                                "-b", "main", "-b", "crash", "-t", String.valueOf(lines))
-                                        .redirectErrorStream(true).start();
-
-                        try { logcat.getOutputStream().close(); } catch (IOException e) {}
-                        try { logcat.getErrorStream().close(); } catch (IOException e) {}
-                        input = new InputStreamReader(logcat.getInputStream());
-
-                        int num;
-                        char[] buf = new char[8192];
-                        while ((num = input.read(buf)) > 0) sb.append(buf, 0, num);
-                    } catch (IOException e) {
-                        Slog.e(TAG, "Error running logcat", e);
-                    } finally {
-                        if (input != null) try { input.close(); } catch (IOException e) {}
-                    }
-                }
-
-                dbox.addText(dropboxTag, sb.toString());
-            }
-        };
+                };
 
         if (runSynchronously) {
             final int oldMask = StrictMode.allowThreadDiskWritesMask();
@@ -10166,43 +10223,48 @@
             mOomAdjuster.dumpCacheOomRankerSettings(pw);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
-
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpAllowedAssociationsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
-
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mPendingIntentController.dumpPendingIntents(pw, dumpAll, dumpPackage);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpBroadcastsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             if (dumpAll || dumpPackage != null) {
                 dumpBroadcastStatsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
             }
             mCpHelper.dumpProvidersLocked(fd, pw, args, opti, dumpAll, dumpPackage);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpPermissions(fd, pw, args, opti, dumpAll, dumpPackage);
             pw.println();
             sdumper = mServices.newServiceDumperLocked(fd, pw, args, opti, dumpAll, dumpPackage);
             if (!dumpClient) {
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 sdumper.dumpLocked();
             }
@@ -10217,7 +10279,8 @@
         // method with the lock held.
         if (dumpClient) {
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             sdumper.dumpWithClient();
         }
@@ -10230,33 +10293,38 @@
             // proxies in the first place.
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpBinderProxies(pw, BINDER_PROXY_HIGH_WATERMARK /* minToDump */);
         }
         synchronized(this) {
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mAtmInternal.dump(DUMP_RECENTS_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                     dumpPackage, displayIdFilter);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mAtmInternal.dump(DUMP_LASTANR_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                     dumpPackage, displayIdFilter);
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mAtmInternal.dump(DUMP_STARTER_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                     dumpPackage, displayIdFilter);
             if (dumpPackage == null) {
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 mAtmInternal.dump(DUMP_CONTAINERS_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                         dumpPackage, displayIdFilter);
@@ -10266,7 +10334,8 @@
             if (!dumpNormalPriority) {
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 mAtmInternal.dump(DUMP_ACTIVITIES_CMD, fd, pw, args, opti, dumpAll, dumpClient,
                         dumpPackage, displayIdFilter);
@@ -10274,45 +10343,53 @@
             if (mAssociations.size() > 0) {
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 dumpAssociationsLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage);
             }
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
                 mProcessList.getAppStartInfoTracker().dumpHistoryProcessStartInfo(pw, dumpPackage);
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
                 mProcessList.mAppExitInfoTracker.dumpHistoryProcessExitInfo(pw, dumpPackage);
             }
             if (dumpPackage == null) {
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 mOomAdjProfiler.dump(pw);
                 pw.println();
                 if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                    pw.println(
+                            "-------------------------------------------------------------------------------");
                 }
                 dumpLmkLocked(pw);
             }
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             synchronized (mProcLock) {
                 mProcessList.dumpProcessesLSP(fd, pw, args, opti, dumpAll, dumpPackage, dumpAppId);
             }
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             dumpUsers(pw);
 
             pw.println();
             if (dumpAll) {
-                pw.println("-------------------------------------------------------------------------------");
+                pw.println(
+                        "-------------------------------------------------------------------------------");
             }
             mComponentAliasResolver.dump(pw);
         }
@@ -10322,7 +10399,8 @@
      * Dump the app restriction controller, it's required not to hold the global lock here.
      */
     private void dumpAppRestrictionController(PrintWriter pw) {
-        pw.println("-------------------------------------------------------------------------------");
+        pw.println(
+                "-------------------------------------------------------------------------------");
         mAppRestrictionController.dump(pw, "");
     }
 
@@ -11462,7 +11540,9 @@
 
     void dumpAllowedAssociationsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
             int opti, boolean dumpAll, String dumpPackage) {
-        pw.println("ACTIVITY MANAGER ALLOWED ASSOCIATION STATE (dumpsys activity allowed-associations)");
+        pw.println(
+                "ACTIVITY MANAGER ALLOWED ASSOCIATION STATE (dumpsys activity"
+                    + " allowed-associations)");
         boolean printed = false;
         if (mAllowedAssociations != null) {
             for (int i = 0; i < mAllowedAssociations.size(); i++) {
@@ -12816,7 +12896,8 @@
             if (!opts.isCompact) {
                 pw.print(" Used RAM: "); pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss
                         + kernelUsed)); pw.print(" (");
-                pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss)); pw.print(" used pss + ");
+                pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss));
+                pw.print(" used pss + ");
                 pw.print(stringifyKBSize(kernelUsed)); pw.print(" kernel)\n");
                 pw.print(" Lost RAM: "); pw.println(stringifyKBSize(lostRAM));
             } else {
@@ -13686,8 +13767,15 @@
         }
         validateServiceInstanceName(instanceName);
 
-        if (DEBUG_SERVICE) Slog.v(TAG_SERVICE,
-                "*** startService: " + service + " type=" + resolvedType + " fg=" + requireForeground);
+        if (DEBUG_SERVICE)
+            Slog.v(
+                    TAG_SERVICE,
+                    "*** startService: "
+                            + service
+                            + " type="
+                            + resolvedType
+                            + " fg="
+                            + requireForeground);
         final int callingPid = Binder.getCallingPid();
         final int callingUid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
@@ -15448,9 +15536,14 @@
             if (checkPermission(android.Manifest.permission.BROADCAST_STICKY,
                     callingPid, callingUid)
                     != PackageManager.PERMISSION_GRANTED) {
-                String msg = "Permission Denial: broadcastIntent() requesting a sticky broadcast from pid="
-                        + callingPid + ", uid=" + callingUid
-                        + " requires " + android.Manifest.permission.BROADCAST_STICKY;
+                String msg =
+                        "Permission Denial: broadcastIntent() requesting a sticky broadcast from"
+                            + " pid="
+                                + callingPid
+                                + ", uid="
+                                + callingUid
+                                + " requires "
+                                + android.Manifest.permission.BROADCAST_STICKY;
                 Slog.w(TAG, msg);
                 throw new SecurityException(msg);
             }
diff --git a/services/core/java/com/android/server/clipboard/ArcClipboardMonitor.java b/services/core/java/com/android/server/clipboard/ArcClipboardMonitor.java
new file mode 100644
index 0000000..413020e
--- /dev/null
+++ b/services/core/java/com/android/server/clipboard/ArcClipboardMonitor.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.clipboard;
+
+import android.annotation.Nullable;
+import android.content.ClipData;
+
+import com.android.server.LocalServices;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class ArcClipboardMonitor implements Consumer<ClipData> {
+    private static final String TAG = "ArcClipboardMonitor";
+
+    public interface ArcClipboardBridge {
+        /**
+         * Called when a clipboard content is updated.
+         */
+        void onPrimaryClipChanged(ClipData data);
+
+        /**
+         * Passes the callback to set a new clipboard content with a uid.
+         */
+        void setHandler(BiConsumer<ClipData, Integer> setAndroidClipboard);
+    }
+
+    private ArcClipboardBridge mBridge;
+    private BiConsumer<ClipData, Integer> mAndroidClipboardSetter;
+
+    ArcClipboardMonitor(final BiConsumer<ClipData, Integer> setAndroidClipboard) {
+        mAndroidClipboardSetter = setAndroidClipboard;
+        LocalServices.addService(ArcClipboardMonitor.class, this);
+    }
+
+    @Override
+    public void accept(final @Nullable ClipData clip) {
+        if (mBridge != null) {
+            mBridge.onPrimaryClipChanged(clip);
+        }
+    }
+
+    /**
+     * Sets the other end of the clipboard bridge.
+     */
+    public void setClipboardBridge(ArcClipboardBridge bridge) {
+        mBridge = bridge;
+        mBridge.setHandler(mAndroidClipboardSetter);
+    }
+}
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 49f6070..4c3020f 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -151,7 +151,7 @@
     private final ContentCaptureManagerInternal mContentCaptureInternal;
     private final AutofillManagerInternal mAutofillInternal;
     private final IBinder mPermissionOwner;
-    private final Consumer<ClipData> mEmulatorClipboardMonitor;
+    private final Consumer<ClipData> mClipboardMonitor;
     private final Handler mWorkerHandler;
 
     @GuardedBy("mLock")
@@ -192,7 +192,7 @@
         final IBinder permOwner = mUgmInternal.newUriPermissionOwner("clipboard");
         mPermissionOwner = permOwner;
         if (Build.IS_EMULATOR) {
-            mEmulatorClipboardMonitor = new EmulatorClipboardMonitor((clip) -> {
+            mClipboardMonitor = new EmulatorClipboardMonitor((clip) -> {
                 synchronized (mLock) {
                     Clipboard clipboard = getClipboardLocked(0, DEVICE_ID_DEFAULT);
                     if (clipboard != null) {
@@ -201,8 +201,12 @@
                     }
                 }
             });
+        } else if (Build.IS_ARC) {
+            mClipboardMonitor = new ArcClipboardMonitor((clip, uid) -> {
+                setPrimaryClipInternal(clip, uid);
+            });
         } else {
-            mEmulatorClipboardMonitor = (clip) -> {};
+            mClipboardMonitor = (clip) -> {};
         }
 
         updateConfig();
@@ -937,7 +941,7 @@
     private void setPrimaryClipInternalLocked(
             @Nullable ClipData clip, int uid, int deviceId, @Nullable String sourcePackage) {
         if (deviceId == DEVICE_ID_DEFAULT) {
-            mEmulatorClipboardMonitor.accept(clip);
+            mClipboardMonitor.accept(clip);
         }
 
         final int userId = UserHandle.getUserId(uid);
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index a43f93a..91e560e 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -20,7 +20,6 @@
 
 import android.annotation.Nullable;
 import android.hardware.display.DisplayManagerInternal;
-import android.os.PowerManager;
 import android.os.Trace;
 
 /**
@@ -110,7 +109,6 @@
         try {
             mDisplayOffloader.stopOffload();
             mIsActive = false;
-            mDisplayPowerController.setBrightnessFromOffload(PowerManager.BRIGHTNESS_INVALID_FLOAT);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 2010aca..7f014f6 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -375,7 +375,8 @@
     // information.
     // At the time of this writing, this value is changed within updatePowerState() only, which is
     // limited to the thread used by DisplayControllerHandler.
-    private final BrightnessReason mBrightnessReason = new BrightnessReason();
+    @VisibleForTesting
+    final BrightnessReason mBrightnessReason = new BrightnessReason();
     private final BrightnessReason mBrightnessReasonTemp = new BrightnessReason();
 
     // Brightness animation ramp rates in brightness units per second
@@ -1379,7 +1380,7 @@
         // Switch to doze auto-brightness mode if needed
         if (mFlags.areAutoBrightnessModesEnabled() && mAutomaticBrightnessController != null
                 && !mAutomaticBrightnessController.isInIdleMode()) {
-            mAutomaticBrightnessController.switchMode(mPowerRequest.policy == POLICY_DOZE
+            mAutomaticBrightnessController.switchMode(Display.isDozeState(state)
                     ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT);
         }
 
@@ -1434,7 +1435,9 @@
         float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness();
         // Apply auto-brightness.
         int brightnessAdjustmentFlags = 0;
-        if (Float.isNaN(brightnessState)) {
+        // AutomaticBrightnessStrategy has higher priority than OffloadBrightnessStrategy
+        if (Float.isNaN(brightnessState)
+                || mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_OFFLOAD) {
             if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) {
                 brightnessState = mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
                         mTempBrightnessEvent);
@@ -1455,8 +1458,11 @@
                     if (mScreenOffBrightnessSensorController != null) {
                         mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
                     }
+                    setBrightnessFromOffload(PowerManager.BRIGHTNESS_INVALID_FLOAT);
                 } else {
                     mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
+                    // Restore the lower-priority brightness strategy
+                    brightnessState = displayBrightnessState.getBrightness();
                 }
             }
         } else {
@@ -3020,9 +3026,10 @@
                     setDwbcLoggingEnabled(msg.arg1);
                     break;
                 case MSG_SET_BRIGHTNESS_FROM_OFFLOAD:
-                    mDisplayBrightnessController.setBrightnessFromOffload(
-                            Float.intBitsToFloat(msg.arg1));
-                    updatePowerState();
+                    if (mDisplayBrightnessController.setBrightnessFromOffload(
+                            Float.intBitsToFloat(msg.arg1))) {
+                        updatePowerState();
+                    }
                     break;
             }
         }
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index f6d02db..34d53be 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -26,6 +26,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.AutomaticBrightnessController;
 import com.android.server.display.BrightnessMappingStrategy;
 import com.android.server.display.BrightnessSetting;
@@ -175,14 +176,19 @@
 
     /**
      * Sets the brightness from the offload session.
+     * @return Whether the offload brightness has changed
      */
-    public void setBrightnessFromOffload(float brightness) {
+    public boolean setBrightnessFromOffload(float brightness) {
         synchronized (mLock) {
-            if (mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy() != null) {
+            if (mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy() != null
+                    && !BrightnessSynchronizer.floatEquals(mDisplayBrightnessStrategySelector
+                    .getOffloadBrightnessStrategy().getOffloadScreenBrightness(), brightness)) {
                 mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()
                         .setOffloadScreenBrightness(brightness);
+                return true;
             }
         }
+        return false;
     }
 
     /**
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
index 8e84450..71cc872 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
@@ -133,7 +133,8 @@
         } else if (BrightnessUtils.isValidBrightnessValue(
                 mTemporaryBrightnessStrategy.getTemporaryScreenBrightness())) {
             displayBrightnessStrategy = mTemporaryBrightnessStrategy;
-        } else if (mOffloadBrightnessStrategy != null && BrightnessUtils.isValidBrightnessValue(
+        } else if (mAutomaticBrightnessStrategy.shouldUseAutoBrightness()
+                && mOffloadBrightnessStrategy != null && BrightnessUtils.isValidBrightnessValue(
                 mOffloadBrightnessStrategy.getOffloadScreenBrightness())) {
             displayBrightnessStrategy = mOffloadBrightnessStrategy;
         }
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index d1ca49b..8b54b22 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -108,7 +108,6 @@
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
                 && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
                 && brightnessReason != BrightnessReason.REASON_OVERRIDE
-                && brightnessReason != BrightnessReason.REASON_OFFLOAD
                 && mAutomaticBrightnessController != null;
         mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
                 && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index fc99471..0ca4808 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -449,7 +449,7 @@
     }
 
     /**
-     * Sends a reliable message rom this client to a nanoapp.
+     * Sends a reliable message from this client to a nanoapp.
      *
      * @param message the message to send
      * @param transactionCallback The callback to use to confirm the delivery of the message for
@@ -473,6 +473,12 @@
             @Nullable IContextHubTransactionCallback transactionCallback) {
         ContextHubServiceUtil.checkPermissions(mContext);
 
+        // Clear the isReliable and messageSequenceNumber fields.
+        // These will be set to true and a real value if the message
+        // is reliable.
+        message.setIsReliable(false);
+        message.setMessageSequenceNumber(0);
+
         @ContextHubTransaction.Result int result;
         if (isRegistered()) {
             int authState = mMessageChannelNanoappIdMap.getOrDefault(
@@ -485,7 +491,9 @@
                 // Return a bland error code for apps targeting old SDKs since they wouldn't be able
                 // to use an error code added in S.
                 return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
-            } else if (authState == AUTHORIZATION_UNKNOWN) {
+            }
+
+            if (authState == AUTHORIZATION_UNKNOWN) {
                 // Only check permissions the first time a nanoapp is queried since nanoapp
                 // permissions don't currently change at runtime. If the host permission changes
                 // later, that'll be checked by onOpChanged.
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index f645eaa..3ecc58e 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -942,6 +942,23 @@
         return false;
     }
 
+    protected boolean isPackageOrComponentAllowedWithPermission(ComponentName component,
+            int userId) {
+        if (!(isPackageOrComponentAllowed(component.flattenToString(), userId)
+                || isPackageOrComponentAllowed(component.getPackageName(), userId))) {
+            return false;
+        }
+        return componentHasBindPermission(component, userId);
+    }
+
+    private boolean componentHasBindPermission(ComponentName component, int userId) {
+        ServiceInfo info = getServiceInfo(component, userId);
+        if (info == null) {
+            return false;
+        }
+        return mConfig.bindPermission.equals(info.permission);
+    }
+
     boolean isPackageOrComponentUserSet(String pkgOrComponent, int userId) {
         synchronized (mApproved) {
             ArraySet<String> services = mUserSetServices.get(userId);
@@ -1003,6 +1020,7 @@
                     for (int uid : uidList) {
                         if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) {
                             anyServicesInvolved = true;
+                            trimApprovedListsForInvalidServices(pkgName, UserHandle.getUserId(uid));
                         }
                     }
                 }
@@ -1135,8 +1153,7 @@
 
         synchronized (mMutex) {
             if (enabled) {
-                if (isPackageOrComponentAllowed(component.flattenToString(), userId)
-                        || isPackageOrComponentAllowed(component.getPackageName(), userId)) {
+                if (isPackageOrComponentAllowedWithPermission(component, userId)) {
                     registerServiceLocked(component, userId);
                 } else {
                     Slog.d(TAG, component + " no longer has permission to be bound");
@@ -1270,6 +1287,33 @@
         return removed;
     }
 
+    private void trimApprovedListsForInvalidServices(String packageName, int userId) {
+        synchronized (mApproved) {
+            final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.get(userId);
+            if (approvedByType == null) {
+                return;
+            }
+            for (int i = 0; i < approvedByType.size(); i++) {
+                final ArraySet<String> approved = approvedByType.valueAt(i);
+                for (int j = approved.size() - 1; j >= 0; j--) {
+                    final String approvedPackageOrComponent = approved.valueAt(j);
+                    if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) {
+                        final ComponentName component = ComponentName.unflattenFromString(
+                                approvedPackageOrComponent);
+                        if (component != null && !componentHasBindPermission(component, userId)) {
+                            approved.removeAt(j);
+                            if (DEBUG) {
+                                Slog.v(TAG, "Removing " + approvedPackageOrComponent
+                                        + " from approved list; no bind permission found "
+                                        + mConfig.bindPermission);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     protected String getPackageName(String packageOrComponent) {
         final ComponentName component = ComponentName.unflattenFromString(packageOrComponent);
         if (component != null) {
@@ -1519,8 +1563,7 @@
     void reregisterService(final ComponentName cn, final int userId) {
         // If rebinding a package that died, ensure it still has permission
         // after the rebind delay
-        if (isPackageOrComponentAllowed(cn.getPackageName(), userId)
-                || isPackageOrComponentAllowed(cn.flattenToString(), userId)) {
+        if (isPackageOrComponentAllowedWithPermission(cn, userId)) {
             registerService(cn, userId);
         }
     }
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 0abe50f..71800ef 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -32,12 +32,13 @@
 import android.app.ondeviceintelligence.IProcessingSignal;
 import android.app.ondeviceintelligence.IResponseCallback;
 import android.app.ondeviceintelligence.IStreamingResponseCallback;
-import android.app.ondeviceintelligence.ITokenCountCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
 import android.app.ondeviceintelligence.OnDeviceIntelligenceManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.ICancellationSignal;
 import android.os.ParcelFileDescriptor;
@@ -47,11 +48,12 @@
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.service.ondeviceintelligence.IOnDeviceIntelligenceService;
-import android.service.ondeviceintelligence.IOnDeviceTrustedInferenceService;
+import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService;
 import android.service.ondeviceintelligence.IRemoteProcessingService;
 import android.service.ondeviceintelligence.IRemoteStorageService;
 import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
 import android.service.ondeviceintelligence.OnDeviceIntelligenceService;
+import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService;
 import android.text.TextUtils;
 import android.util.Slog;
 
@@ -69,7 +71,7 @@
  * This is the system service for handling calls on the
  * {@link android.app.ondeviceintelligence.OnDeviceIntelligenceManager}. This
  * service holds connection references to the underlying remote services i.e. the isolated service
- * {@link  OnDeviceTrustedInferenceService} and a regular
+ * {@link OnDeviceSandboxedInferenceService} and a regular
  * service counter part {@link OnDeviceIntelligenceService}.
  *
  * Note: Both the remote services run under the SYSTEM user, as we cannot have separate instance of
@@ -90,7 +92,7 @@
     protected final Object mLock = new Object();
 
 
-    private RemoteOnDeviceTrustedInferenceService mRemoteInferenceService;
+    private RemoteOnDeviceSandboxedInferenceService mRemoteInferenceService;
     private RemoteOnDeviceIntelligenceService mRemoteOnDeviceIntelligenceService;
     volatile boolean mIsServiceEnabled;
 
@@ -165,7 +167,7 @@
             }
             ensureRemoteIntelligenceServiceInitialized();
             mRemoteOnDeviceIntelligenceService.post(
-                    service -> service.getFeature(id, featureCallback));
+                    service -> service.getFeature(Binder.getCallingUid(), id, featureCallback));
         }
 
         @Override
@@ -185,7 +187,7 @@
             }
             ensureRemoteIntelligenceServiceInitialized();
             mRemoteOnDeviceIntelligenceService.post(
-                    service -> service.listFeatures(listFeaturesCallback));
+                    service -> service.listFeatures(Binder.getCallingUid(), listFeaturesCallback));
         }
 
         @Override
@@ -207,7 +209,8 @@
             }
             ensureRemoteIntelligenceServiceInitialized();
             mRemoteOnDeviceIntelligenceService.post(
-                    service -> service.getFeatureDetails(feature, featureDetailsCallback));
+                    service -> service.getFeatureDetails(Binder.getCallingUid(), feature,
+                            featureDetailsCallback));
         }
 
         @Override
@@ -227,33 +230,35 @@
             }
             ensureRemoteIntelligenceServiceInitialized();
             mRemoteOnDeviceIntelligenceService.post(
-                    service -> service.requestFeatureDownload(feature, cancellationSignal,
+                    service -> service.requestFeatureDownload(Binder.getCallingUid(), feature,
+                            cancellationSignal,
                             downloadCallback));
         }
 
 
         @Override
-        public void requestTokenCount(Feature feature,
+        public void requestTokenInfo(Feature feature,
                 Content request, ICancellationSignal cancellationSignal,
-                ITokenCountCallback tokenCountcallback) throws RemoteException {
+                ITokenInfoCallback tokenInfoCallback) throws RemoteException {
             Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing");
             Objects.requireNonNull(feature);
             Objects.requireNonNull(request);
-            Objects.requireNonNull(tokenCountcallback);
+            Objects.requireNonNull(tokenInfoCallback);
 
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
             if (!mIsServiceEnabled) {
                 Slog.w(TAG, "Service not available");
-                tokenCountcallback.onFailure(
+                tokenInfoCallback.onFailure(
                         OnDeviceIntelligenceManager.OnDeviceIntelligenceManagerException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE,
                         "OnDeviceIntelligenceManagerService is unavailable",
                         new PersistableBundle());
             }
-            ensureRemoteTrustedInferenceServiceInitialized();
+            ensureRemoteInferenceServiceInitialized();
             mRemoteInferenceService.post(
-                    service -> service.requestTokenCount(feature, request, cancellationSignal,
-                            tokenCountcallback));
+                    service -> service.requestTokenInfo(Binder.getCallingUid(), feature, request,
+                            cancellationSignal,
+                            tokenInfoCallback));
         }
 
         @Override
@@ -267,7 +272,6 @@
             Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest");
             Objects.requireNonNull(feature);
             Objects.requireNonNull(responseCallback);
-            Objects.requireNonNull(request);
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
             if (!mIsServiceEnabled) {
@@ -277,9 +281,10 @@
                         "OnDeviceIntelligenceManagerService is unavailable",
                         new PersistableBundle());
             }
-            ensureRemoteTrustedInferenceServiceInitialized();
+            ensureRemoteInferenceServiceInitialized();
             mRemoteInferenceService.post(
-                    service -> service.processRequest(feature, request, requestType,
+                    service -> service.processRequest(Binder.getCallingUid(), feature, request,
+                            requestType,
                             cancellationSignal, processingSignal,
                             responseCallback));
         }
@@ -293,7 +298,6 @@
                 IStreamingResponseCallback streamingCallback) throws RemoteException {
             Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming");
             Objects.requireNonNull(feature);
-            Objects.requireNonNull(request);
             Objects.requireNonNull(streamingCallback);
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
@@ -304,9 +308,10 @@
                         "OnDeviceIntelligenceManagerService is unavailable",
                         new PersistableBundle());
             }
-            ensureRemoteTrustedInferenceServiceInitialized();
+            ensureRemoteInferenceServiceInitialized();
             mRemoteInferenceService.post(
-                    service -> service.processRequestStreaming(feature, request, requestType,
+                    service -> service.processRequestStreaming(Binder.getCallingUid(), feature,
+                            request, requestType,
                             cancellationSignal, processingSignal,
                             streamingCallback));
         }
@@ -346,7 +351,7 @@
                     Bundle processingState,
                     IProcessingUpdateStatusCallback callback) {
                 try {
-                    ensureRemoteTrustedInferenceServiceInitialized();
+                    ensureRemoteInferenceServiceInitialized();
                     mRemoteInferenceService.post(
                             service -> service.updateProcessingState(
                                     processingState, callback));
@@ -363,22 +368,24 @@
         };
     }
 
-    private void ensureRemoteTrustedInferenceServiceInitialized() throws RemoteException {
+    private void ensureRemoteInferenceServiceInitialized() throws RemoteException {
         synchronized (mLock) {
             if (mRemoteInferenceService == null) {
                 String serviceName = mContext.getResources().getString(
-                        R.string.config_defaultOnDeviceTrustedInferenceService);
+                        R.string.config_defaultOnDeviceSandboxedInferenceService);
                 validateService(serviceName, true);
-                mRemoteInferenceService = new RemoteOnDeviceTrustedInferenceService(mContext,
+                mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext,
                         ComponentName.unflattenFromString(serviceName),
                         UserHandle.SYSTEM.getIdentifier());
                 mRemoteInferenceService.setServiceLifecycleCallbacks(
                         new ServiceConnector.ServiceLifecycleCallbacks<>() {
                             @Override
                             public void onConnected(
-                                    @NonNull IOnDeviceTrustedInferenceService service) {
+                                    @NonNull IOnDeviceSandboxedInferenceService service) {
                                 try {
                                     ensureRemoteIntelligenceServiceInitialized();
+                                    mRemoteOnDeviceIntelligenceService.post(
+                                            intelligenceService -> intelligenceService.notifyInferenceServiceConnected());
                                     service.registerRemoteStorageService(
                                             getIRemoteStorageService());
                                 } catch (RemoteException ex) {
@@ -433,7 +440,7 @@
             }
 
             checkServiceRequiresPermission(serviceInfo,
-                    Manifest.permission.BIND_ON_DEVICE_TRUSTED_SERVICE);
+                    Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
             if (!isIsolatedService(serviceInfo)) {
                 throw new SecurityException(
                         "Call required an isolated service, but the configured service: "
diff --git a/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceTrustedInferenceService.java b/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
similarity index 73%
rename from services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceTrustedInferenceService.java
rename to services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
index cc8e788..69ba1d2 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceTrustedInferenceService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
@@ -22,18 +22,18 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.service.ondeviceintelligence.IOnDeviceTrustedInferenceService;
-import android.service.ondeviceintelligence.OnDeviceTrustedInferenceService;
+import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService;
+import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService;
 
 import com.android.internal.infra.ServiceConnector;
 
 
 /**
- * Manages the connection to the remote on-device trusted inference service. Also, handles unbinding
+ * Manages the connection to the remote on-device sand boxed inference service. Also, handles unbinding
  * logic set by the service implementation via a SecureSettings flag.
  */
-public class RemoteOnDeviceTrustedInferenceService extends
-        ServiceConnector.Impl<IOnDeviceTrustedInferenceService> {
+public class RemoteOnDeviceSandboxedInferenceService extends
+        ServiceConnector.Impl<IOnDeviceSandboxedInferenceService> {
     /**
      * Creates an instance of {@link ServiceConnector}
      *
@@ -43,12 +43,12 @@
      *                {@link Context#unbindService unbinding}
      * @param userId  to be used for {@link Context#bindServiceAsUser binding}
      */
-    RemoteOnDeviceTrustedInferenceService(Context context, ComponentName serviceName,
+    RemoteOnDeviceSandboxedInferenceService(Context context, ComponentName serviceName,
             int userId) {
         super(context, new Intent(
-                        OnDeviceTrustedInferenceService.SERVICE_INTERFACE).setComponent(serviceName),
+                        OnDeviceSandboxedInferenceService.SERVICE_INTERFACE).setComponent(serviceName),
                 BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES, userId,
-                IOnDeviceTrustedInferenceService.Stub::asInterface);
+                IOnDeviceSandboxedInferenceService.Stub::asInterface);
 
         // Bind right away
         connect();
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index ef8453d..29de26e 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -31,6 +31,7 @@
 import static android.content.pm.PackageManager.DELETE_ALL_USERS;
 import static android.content.pm.PackageManager.DELETE_ARCHIVE;
 import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
+import static android.content.pm.PackageManager.INSTALL_UNARCHIVE;
 import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
 import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
 import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE;
@@ -754,8 +755,9 @@
 
         int draftSessionId;
         try {
-            draftSessionId = Binder.withCleanCallingIdentity(() ->
-                    createDraftSession(packageName, installerPackage, statusReceiver, userId));
+            draftSessionId = Binder.withCleanCallingIdentity(
+                    () -> createDraftSession(packageName, installerPackage, callerPackageName,
+                            statusReceiver, userId));
         } catch (RuntimeException e) {
             if (e.getCause() instanceof IOException) {
                 throw ExceptionUtils.wrap((IOException) e.getCause());
@@ -795,11 +797,18 @@
     }
 
     private int createDraftSession(String packageName, String installerPackage,
+            String callerPackageName,
             IntentSender statusReceiver, int userId) throws IOException {
         PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
                 PackageInstaller.SessionParams.MODE_FULL_INSTALL);
         sessionParams.setAppPackageName(packageName);
-        sessionParams.installFlags = INSTALL_UNARCHIVE_DRAFT;
+        sessionParams.setAppLabel(
+                mContext.getString(com.android.internal.R.string.unarchival_session_app_label));
+        sessionParams.setAppIcon(
+                getArchivedAppIcon(packageName, UserHandle.of(userId), callerPackageName));
+        // To make sure SessionInfo::isUnarchival returns true for draft sessions,
+        // INSTALL_UNARCHIVE is also set.
+        sessionParams.installFlags = (INSTALL_UNARCHIVE_DRAFT | INSTALL_UNARCHIVE);
 
         int installerUid = mPm.snapshotComputer().getPackageUid(installerPackage, 0, userId);
         // Handles case of repeated unarchival calls for the same package.
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 24d7acd..5360788 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -700,6 +700,8 @@
                     return runOverrideStatus();
                 case "reset":
                     return runReset();
+                case "headroom":
+                    return runHeadroom();
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -862,6 +864,36 @@
             }
         }
 
+        private int runHeadroom() {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                final PrintWriter pw = getOutPrintWriter();
+                int forecastSecs;
+                try {
+                    forecastSecs = Integer.parseInt(getNextArgRequired());
+                } catch (RuntimeException ex) {
+                    pw.println("Error: " + ex);
+                    return -1;
+                }
+                if (!mHalReady.get()) {
+                    pw.println("Error: thermal HAL is not ready");
+                    return -1;
+                }
+
+                if (forecastSecs < MIN_FORECAST_SEC || forecastSecs > MAX_FORECAST_SEC) {
+                    pw.println(
+                            "Error: forecast second input should be in range [" + MIN_FORECAST_SEC
+                                    + "," + MAX_FORECAST_SEC + "]");
+                    return -1;
+                }
+                float headroom = mTemperatureWatcher.getForecast(forecastSecs);
+                pw.println("Headroom in " + forecastSecs + " seconds: " + headroom);
+                return 0;
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
         @Override
         public void onHelp() {
             final PrintWriter pw = getOutPrintWriter();
@@ -877,6 +909,9 @@
             pw.println("    status code is defined in android.os.Temperature.");
             pw.println("  reset");
             pw.println("    unlocks the thermal status of the device.");
+            pw.println("  headroom FORECAST_SECONDS");
+            pw.println("    gets the thermal headroom forecast in specified seconds, from ["
+                    + MIN_FORECAST_SEC + "," + MAX_FORECAST_SEC + "].");
             pw.println();
         }
     }
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 ffce50e..79adcb4 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -349,7 +349,6 @@
             }
         }
 
-        userState.mIAppMap.clear();
         userState.mAdServiceMap = adServiceMap;
     }
 
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 9616c28..5175b74 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -131,19 +131,23 @@
                     (bitmapSize.y - crop.height()) / 2);
             return crop;
         }
+
+        // If any suggested crop is invalid, fallback to case 1
+        for (int i = 0; i < suggestedCrops.size(); i++) {
+            Rect testCrop = suggestedCrops.valueAt(i);
+            if (testCrop == null || testCrop.left < 0 || testCrop.top < 0
+                    || testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) {
+                Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize);
+                return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+            }
+        }
+
         int orientation = getOrientation(displaySize);
 
         // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop
         Rect suggestedCrop = suggestedCrops.get(orientation);
         if (suggestedCrop != null) {
-            if (suggestedCrop.left < 0 || suggestedCrop.top < 0
-                    || suggestedCrop.right > bitmapSize.x || suggestedCrop.bottom > bitmapSize.y) {
-                Slog.w(TAG, "invalid suggested crop: " + suggestedCrop);
-                Rect fullImage = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
-                return getAdjustedCrop(fullImage, bitmapSize, displaySize, true, rtl, ADD);
-            } else {
                 return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, rtl, ADD);
-            }
         }
 
         // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and
@@ -247,6 +251,7 @@
         Rect adjustedCrop = new Rect(crop);
         float cropRatio = ((float) crop.width()) / crop.height();
         float screenRatio = ((float) screenSize.x) / screenSize.y;
+        if (cropRatio == screenRatio) return crop;
         if (cropRatio > screenRatio) {
             if (!parallax) {
                 // rotate everything 90 degrees clockwise, compute the result, and rotate back
@@ -276,6 +281,7 @@
                 }
             }
         } else {
+            // TODO (b/281648899) the third case is not always correct, fix that.
             int widthToAdd = mode == REMOVE ? 0
                     : mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width())
                     : (int) (0.5 + crop.height() - crop.width());
@@ -646,6 +652,9 @@
         if (!success) {
             Slog.e(TAG, "Unable to apply new wallpaper");
             wallpaper.getCropFile().delete();
+            wallpaper.mCropHints.clear();
+            wallpaper.cropHint.set(0, 0, 0, 0);
+            wallpaper.mSampleSize = 1f;
         }
 
         if (wallpaper.getCropFile().exists()) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index 88e9672..0165d65 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -341,6 +341,7 @@
             } else {
                 wallpaper.cropHint.set(totalCropHint);
             }
+            wallpaper.mSampleSize = parser.getAttributeFloat(null, "sampleSize", 1f);
         } else {
             wallpaper.cropHint.set(totalCropHint);
         }
@@ -493,6 +494,7 @@
             out.attributeInt(null, "totalCropTop", wallpaper.cropHint.top);
             out.attributeInt(null, "totalCropRight", wallpaper.cropHint.right);
             out.attributeInt(null, "totalCropBottom", wallpaper.cropHint.bottom);
+            out.attributeFloat(null, "sampleSize", wallpaper.mSampleSize);
         } else if (!multiCrop()) {
             final DisplayData wpdData =
                     mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY);
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index b88d7f7..83f44d2 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -520,37 +520,11 @@
         updateVisibility();
         mControl = new InsetsSourceControl(mSource.getId(), mSource.getType(), leash,
                 mClientVisible, surfacePosition, getInsetsHint());
-        mStateController.notifySurfaceTransactionReady(this, getSurfaceTransactionId(leash), true);
 
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
                 "InsetsSource Control %s for target %s", mControl, mControlTarget);
     }
 
-    private long getSurfaceTransactionId(SurfaceControl leash) {
-        // Here returns mNativeObject (long) as the ID instead of the leash itself so that
-        // InsetsStateController won't keep referencing the leash unexpectedly.
-        return leash != null ? leash.mNativeObject : 0;
-    }
-
-    /**
-     * This is called when the surface transaction of the leash initialization has been committed.
-     *
-     * @param id Indicates which transaction is committed so that stale callbacks can be dropped.
-     */
-    void onSurfaceTransactionCommitted(long id) {
-        if (mIsLeashReadyForDispatching) {
-            return;
-        }
-        if (mControl == null) {
-            return;
-        }
-        if (id != getSurfaceTransactionId(mControl.getLeash())) {
-            return;
-        }
-        mIsLeashReadyForDispatching = true;
-        mStateController.notifySurfaceTransactionReady(this, 0, false);
-    }
-
     void startSeamlessRotation() {
         if (!mSeamlessRotating) {
             mSeamlessRotating = true;
@@ -571,6 +545,10 @@
         return true;
     }
 
+    void onSurfaceTransactionApplied() {
+        mIsLeashReadyForDispatching = true;
+    }
+
     void setClientVisible(boolean clientVisible) {
         if (mClientVisible == clientVisible) {
             return;
@@ -755,7 +733,6 @@
         public void onAnimationCancelled(SurfaceControl animationLeash) {
             if (mAdapter == this) {
                 mStateController.notifyControlRevoked(mControlTarget, InsetsSourceProvider.this);
-                mStateController.notifySurfaceTransactionReady(InsetsSourceProvider.this, 0, false);
                 mControl = null;
                 mControlTarget = null;
                 mAdapter = null;
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index ba578f6..6b9fcf4 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -34,7 +34,6 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
-import android.util.SparseLongArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
@@ -59,7 +58,6 @@
     private final DisplayContent mDisplayContent;
 
     private final SparseArray<InsetsSourceProvider> mProviders = new SparseArray<>();
-    private final SparseLongArray mSurfaceTransactionIds = new SparseLongArray();
     private final ArrayMap<InsetsControlTarget, ArrayList<InsetsSourceProvider>>
             mControlTargetProvidersMap = new ArrayMap<>();
     private final SparseArray<InsetsControlTarget> mIdControlTargetMap = new SparseArray<>();
@@ -362,32 +360,14 @@
         notifyPendingInsetsControlChanged();
     }
 
-    void notifySurfaceTransactionReady(InsetsSourceProvider provider, long id, boolean ready) {
-        if (ready) {
-            mSurfaceTransactionIds.put(provider.getSource().getId(), id);
-        } else {
-            mSurfaceTransactionIds.delete(provider.getSource().getId());
-        }
-    }
-
     private void notifyPendingInsetsControlChanged() {
         if (mPendingControlChanged.isEmpty()) {
             return;
         }
-        final int size = mSurfaceTransactionIds.size();
-        final SparseLongArray surfaceTransactionIds = new SparseLongArray(size);
-        for (int i = 0; i < size; i++) {
-            surfaceTransactionIds.append(
-                    mSurfaceTransactionIds.keyAt(i), mSurfaceTransactionIds.valueAt(i));
-        }
         mDisplayContent.mWmService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
-            for (int i = 0; i < size; i++) {
-                final int sourceId = surfaceTransactionIds.keyAt(i);
-                final InsetsSourceProvider provider = mProviders.get(sourceId);
-                if (provider == null) {
-                    continue;
-                }
-                provider.onSurfaceTransactionCommitted(surfaceTransactionIds.valueAt(i));
+            for (int i = mProviders.size() - 1; i >= 0; i--) {
+                final InsetsSourceProvider provider = mProviders.valueAt(i);
+                provider.onSurfaceTransactionApplied();
             }
             final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>();
             int displayId = mDisplayContent.getDisplayId();
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 5e7f1cb..b43a454 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -28,7 +28,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.content.Context;
-import android.os.HandlerExecutor;
 import android.os.Trace;
 import android.util.Slog;
 import android.util.TimeUtils;
@@ -70,8 +69,6 @@
 
     private Choreographer mChoreographer;
 
-    private final HandlerExecutor mExecutor;
-
     /**
      * Indicates whether we have an animation frame callback scheduled, which will happen at
      * vsync-app and then schedule the animation tick at the right time (vsync-sf).
@@ -83,7 +80,8 @@
      * A list of runnable that need to be run after {@link WindowContainer#prepareSurfaces} is
      * executed and the corresponding transaction is closed and applied.
      */
-    private ArrayList<Runnable> mAfterPrepareSurfacesRunnables = new ArrayList<>();
+    private final ArrayList<Runnable> mAfterPrepareSurfacesRunnables = new ArrayList<>();
+    private boolean mInExecuteAfterPrepareSurfacesRunnables;
 
     private final SurfaceControl.Transaction mTransaction;
 
@@ -94,7 +92,6 @@
         mTransaction = service.mTransactionFactory.get();
         service.mAnimationHandler.runWithScissors(
                 () -> mChoreographer = Choreographer.getSfInstance(), 0 /* timeout */);
-        mExecutor = new HandlerExecutor(service.mAnimationHandler);
 
         mAnimationFrameCallback = frameTimeNs -> {
             synchronized (mService.mGlobalLock) {
@@ -200,19 +197,6 @@
             updateRunningExpensiveAnimationsLegacy();
         }
 
-        final ArrayList<Runnable> afterPrepareSurfacesRunnables = mAfterPrepareSurfacesRunnables;
-        if (!afterPrepareSurfacesRunnables.isEmpty()) {
-            mAfterPrepareSurfacesRunnables = new ArrayList<>();
-            mTransaction.addTransactionCommittedListener(mExecutor, () -> {
-                synchronized (mService.mGlobalLock) {
-                    // Traverse in order they were added.
-                    for (int i = 0, size = afterPrepareSurfacesRunnables.size(); i < size; i++) {
-                        afterPrepareSurfacesRunnables.get(i).run();
-                    }
-                    afterPrepareSurfacesRunnables.clear();
-                }
-            });
-        }
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "applyTransaction");
         mTransaction.apply();
         Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
@@ -220,6 +204,7 @@
         ProtoLog.i(WM_SHOW_TRANSACTIONS, "<<< CLOSE TRANSACTION animate");
 
         mService.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
+        executeAfterPrepareSurfacesRunnables();
 
         if (DEBUG_WINDOW_TRACE) {
             Slog.i(TAG, "!!! animate: exit"
@@ -301,10 +286,34 @@
 
     /**
      * Adds a runnable to be executed after {@link WindowContainer#prepareSurfaces} is called and
-     * the corresponding transaction is closed, applied, and committed.
+     * the corresponding transaction is closed and applied.
      */
     void addAfterPrepareSurfacesRunnable(Runnable r) {
+        // If runnables are already being handled in executeAfterPrepareSurfacesRunnable, then just
+        // immediately execute the runnable passed in.
+        if (mInExecuteAfterPrepareSurfacesRunnables) {
+            r.run();
+            return;
+        }
+
         mAfterPrepareSurfacesRunnables.add(r);
         scheduleAnimation();
     }
+
+    void executeAfterPrepareSurfacesRunnables() {
+
+        // Don't even think about to start recursing!
+        if (mInExecuteAfterPrepareSurfacesRunnables) {
+            return;
+        }
+        mInExecuteAfterPrepareSurfacesRunnables = true;
+
+        // Traverse in order they were added.
+        final int size = mAfterPrepareSurfacesRunnables.size();
+        for (int i = 0; i < size; i++) {
+            mAfterPrepareSurfacesRunnables.get(i).run();
+        }
+        mAfterPrepareSurfacesRunnables.clear();
+        mInExecuteAfterPrepareSurfacesRunnables = false;
+    }
 }
diff --git a/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java b/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java
index e12e961f..180f54e 100644
--- a/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java
+++ b/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java
@@ -20,8 +20,6 @@
 import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6;
 import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
 
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -31,19 +29,15 @@
 
 import android.net.ipsec.ike.IkeKeyIdIdentification;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-import android.test.mock.MockContext;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.net.VpnProfile;
 import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
 import com.android.net.module.util.ProxyUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -62,8 +56,7 @@
 
 /** Unit tests for {@link Ikev2VpnProfile.Builder}. */
 @SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(AndroidJUnit4.class)
 public class Ikev2VpnProfileTest {
     private static final String SERVER_ADDR_STRING = "1.2.3.4";
     private static final String IDENTITY_STRING = "Identity";
@@ -73,16 +66,6 @@
     private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
     private static final int TEST_MTU = 1300;
 
-    @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
-    private final MockContext mMockContext =
-            new MockContext() {
-                @Override
-                public String getOpPackageName() {
-                    return "fooPackage";
-                }
-            };
     private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy(
             SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST));
 
@@ -227,7 +210,6 @@
     @Test
     public void testSetAllowedAlgorithmsInvalidList() throws Exception {
         final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        List<String> allowedAlgorithms = new ArrayList<>();
 
         try {
             builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256));
@@ -245,7 +227,6 @@
     @Test
     public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception {
         final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        List<String> allowedAlgorithms = new ArrayList<>();
 
         try {
             builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5));
@@ -271,9 +252,6 @@
         }
     }
 
-
-    // TODO: Refer to Build.VERSION_CODES.SC_V2 when it's available in AOSP and mainline branch
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
     @Test
     public void testBuildExcludeLocalRoutesSet() throws Exception {
         final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
diff --git a/services/tests/VpnTests/java/android/net/VpnManagerTest.java b/services/tests/VpnTests/java/android/net/VpnManagerTest.java
index 365b4d1..f5b83f0 100644
--- a/services/tests/VpnTests/java/android/net/VpnManagerTest.java
+++ b/services/tests/VpnTests/java/android/net/VpnManagerTest.java
@@ -29,17 +29,15 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.os.Build;
 import android.test.mock.MockContext;
 import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.net.VpnProfile;
 import com.android.internal.util.MessageUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -47,13 +45,11 @@
 
 /** Unit tests for {@link VpnManager}. */
 @SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(AndroidJUnit4.class)
 public class VpnManagerTest {
 
     private static final String PKG_NAME = "fooPackage";
 
-    private static final String SESSION_NAME_STRING = "testSession";
     private static final String SERVER_ADDR_STRING = "1.2.3.4";
     private static final String IDENTITY_STRING = "Identity";
     private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
diff --git a/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java b/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java
index acae7d2..acbe8b8 100644
--- a/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java
+++ b/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java
@@ -19,8 +19,6 @@
 import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
 import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
 
-import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
-import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
 import static com.android.testutils.ParcelUtils.assertParcelSane;
 
 import static org.junit.Assert.assertEquals;
@@ -32,13 +30,10 @@
 
 import android.net.IpSecAlgorithm;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -48,8 +43,7 @@
 
 /** Unit tests for {@link VpnProfile}. */
 @SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(AndroidJUnit4.class)
 public class VpnProfileTest {
     private static final String DUMMY_PROFILE_KEY = "Test";
 
@@ -176,17 +170,8 @@
 
     @Test
     public void testParcelUnparcel() {
-        if (isAtLeastU()) {
-            // automaticNattKeepaliveTimerEnabled, automaticIpVersionSelectionEnabled added in U.
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28);
-            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28);
-        } else if (isAtLeastT()) {
-            // excludeLocalRoutes, requiresPlatformValidation were added in T.
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 26);
-            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 26);
-        } else {
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
-        }
+        assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28);
+        assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28);
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
index fbb14c3..4409051 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
@@ -26,7 +26,6 @@
 import static org.mockito.Mockito.when;
 
 import android.hardware.display.DisplayManagerInternal;
-import android.os.PowerManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -66,8 +65,6 @@
         mSession.stopOffload();
 
         assertFalse(mSession.isActive());
-        verify(mDisplayPowerController).setBrightnessFromOffload(
-                PowerManager.BRIGHTNESS_INVALID_FLOAT);
 
         // An inactive session shouldn't be stopped again
         mSession.stopOffload();
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index e9315c8..14d8a9c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -22,6 +22,7 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -77,6 +78,7 @@
 import com.android.server.am.BatteryStatsService;
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.BrightnessReason;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
@@ -1559,6 +1561,43 @@
 
         verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
                 eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        assertEquals(BrightnessReason.REASON_OFFLOAD, mHolder.dpc.mBrightnessReason.getReason());
+    }
+
+    @Test
+    public void testBrightness_AutomaticHigherPriorityThanOffload() {
+        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        float brightness = 0.34f;
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        mHolder.dpc.setBrightnessFromOffload(brightness);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        assertEquals(BrightnessReason.REASON_OFFLOAD, mHolder.dpc.mBrightnessReason.getReason());
+
+        // Now automatic brightness becomes available
+        brightness = 0.22f;
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(brightness);
+
+        mHolder.dpc.updateBrightness();
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(),
+                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        assertEquals(BrightnessReason.REASON_AUTOMATIC, mHolder.dpc.mBrightnessReason.getReason());
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index b99ecf3..14de527 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -46,7 +46,6 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.PowerManager;
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.SurfaceControl;
@@ -1229,8 +1228,6 @@
 
         verify(mDisplayOffloader).stopOffload();
         assertFalse(mDisplayOffloadSession.isActive());
-        verify(mMockedDisplayPowerController).setBrightnessFromOffload(
-                PowerManager.BRIGHTNESS_INVALID_FLOAT);
     }
 
     private void initDisplayOffloadSession() {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
index 289d54b..9b6cc0a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
@@ -393,7 +393,31 @@
         OffloadBrightnessStrategy offloadBrightnessStrategy = mock(OffloadBrightnessStrategy.class);
         when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn(
                 offloadBrightnessStrategy);
-        mDisplayBrightnessController.setBrightnessFromOffload(brightness);
+        boolean brightnessUpdated =
+                mDisplayBrightnessController.setBrightnessFromOffload(brightness);
         verify(offloadBrightnessStrategy).setOffloadScreenBrightness(brightness);
+        assertTrue(brightnessUpdated);
+    }
+
+    @Test
+    public void setBrightnessFromOffload_OffloadStrategyNull() {
+        float brightness = 0.4f;
+        when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn(null);
+        boolean brightnessUpdated =
+                mDisplayBrightnessController.setBrightnessFromOffload(brightness);
+        assertFalse(brightnessUpdated);
+    }
+
+    @Test
+    public void setBrightnessFromOffload_BrightnessUnchanged() {
+        float brightness = 0.4f;
+        OffloadBrightnessStrategy offloadBrightnessStrategy = mock(OffloadBrightnessStrategy.class);
+        when(offloadBrightnessStrategy.getOffloadScreenBrightness()).thenReturn(brightness);
+        when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn(
+                offloadBrightnessStrategy);
+        boolean brightnessUpdated =
+                mDisplayBrightnessController.setBrightnessFromOffload(brightness);
+        verify(offloadBrightnessStrategy, never()).setOffloadScreenBrightness(brightness);
+        assertFalse(brightnessUpdated);
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
index 1c681ce..0e89d83 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
@@ -247,6 +247,7 @@
         displayPowerRequest.screenBrightnessOverride = Float.NaN;
         when(mFollowerBrightnessStrategy.getBrightnessToFollow()).thenReturn(Float.NaN);
         when(mTemporaryBrightnessStrategy.getTemporaryScreenBrightness()).thenReturn(Float.NaN);
+        when(mAutomaticBrightnessStrategy.shouldUseAutoBrightness()).thenReturn(true);
         when(mOffloadBrightnessStrategy.getOffloadScreenBrightness()).thenReturn(0.3f);
         assertEquals(mOffloadBrightnessStrategy, mDisplayBrightnessStrategySelector.selectStrategy(
                 displayPowerRequest, Display.STATE_ON));
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
index ba462e3..a5dc668 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -188,29 +188,6 @@
     }
 
     @Test
-    public void testAutoBrightnessState_BrightnessReasonIsOffload() {
-        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
-        int targetDisplayState = Display.STATE_ON;
-        boolean allowAutoBrightnessWhileDozing = false;
-        int brightnessReason = BrightnessReason.REASON_OFFLOAD;
-        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
-        float lastUserSetBrightness = 0.2f;
-        boolean userSetBrightnessChanged = true;
-        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
-        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
-                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
-                userSetBrightnessChanged);
-        verify(mAutomaticBrightnessController)
-                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED,
-                        mBrightnessConfiguration,
-                        lastUserSetBrightness,
-                        userSetBrightnessChanged, 0.5f,
-                        false, policy, true);
-        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
-        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
-    }
-
-    @Test
     public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_DOZE;
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 6bcd778..c6a6865 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
@@ -60,10 +60,13 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.app.AlarmManager;
 import android.app.AppGlobals;
 import android.app.job.JobInfo;
@@ -71,12 +74,15 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
 import android.net.NetworkRequest;
 import android.os.Looper;
 import android.os.PowerManager;
+import android.os.UserHandle;
 import android.provider.DeviceConfig;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccSlotMapping;
 import android.util.ArraySet;
 import android.util.EmptyArray;
 import android.util.SparseArray;
@@ -104,6 +110,9 @@
 import java.time.Instant;
 import java.time.ZoneOffset;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 public class FlexibilityControllerTest {
@@ -113,6 +122,9 @@
 
     private MockitoSession mMockingSession;
     private BroadcastReceiver mBroadcastReceiver;
+    private final SparseArray<ArraySet<String>> mCarrierPrivilegedApps = new SparseArray<>();
+    private final SparseArray<TelephonyManager.CarrierPrivilegesCallback>
+            mCarrierPrivilegedCallbacks = new SparseArray<>();
     private FlexibilityController mFlexibilityController;
     private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
     private JobStore mJobStore;
@@ -130,6 +142,10 @@
     @Mock
     private PrefetchController mPrefetchController;
     @Mock
+    private TelephonyManager mTelephonyManager;
+    @Mock
+    private IPackageManager mIPackageManager;
+    @Mock
     private PackageManager mPackageManager;
 
     @Before
@@ -138,6 +154,7 @@
                 .initMocks(this)
                 .strictness(Strictness.LENIENT)
                 .spyStatic(DeviceConfig.class)
+                .mockStatic(AppGlobals.class)
                 .mockStatic(LocalServices.class)
                 .startMocking();
         // Called in StateController constructor.
@@ -167,17 +184,23 @@
                         -> mDeviceConfigPropertiesBuilder.build())
                 .when(() -> DeviceConfig.getProperties(
                         eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>any()));
+        // Used in FlexibilityController.SpecialAppTracker.
+        when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION))
+                .thenReturn(true);
         //used to get jobs by UID
         mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir());
         doReturn(mJobStore).when(mJobSchedulerService).getJobStore();
         // Used in JobStatus.
-        doReturn(mock(PackageManagerInternal.class))
-                .when(() -> LocalServices.getService(PackageManagerInternal.class));
+        doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
         // Freeze the clocks at a moment in time
         JobSchedulerService.sSystemClock =
                 Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC);
         JobSchedulerService.sElapsedRealtimeClock =
                 Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC);
+        // Set empty set of privileged apps.
+        setSimSlotMappings(null);
+        setPowerWhitelistExceptIdle();
         // Initialize real objects.
         doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(any());
         ArgumentCaptor<BroadcastReceiver> receiverCaptor =
@@ -249,9 +272,13 @@
     }
 
     private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
+        return createJobStatus(testTag, job, SOURCE_PACKAGE);
+    }
+
+    private JobStatus createJobStatus(String testTag, JobInfo.Builder job, String sourcePackage) {
         JobInfo jobInfo = job.build();
         JobStatus js = JobStatus.createFromJobInfo(
-                jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag);
+                jobInfo, 1000, sourcePackage, SOURCE_USER_ID, "FCTest", testTag);
         js.enqueueTime = FROZEN_TIME;
         js.setStandbyBucket(ACTIVE_INDEX);
         if (js.hasFlexibilityConstraint()) {
@@ -1084,7 +1111,6 @@
 
     @Test
     public void testAllowlistedAppBypass() {
-        setPowerWhitelistExceptIdle();
         mFlexibilityController.onSystemServicesReady();
 
         JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
@@ -1118,6 +1144,148 @@
     }
 
     @Test
+    public void testCarrierPrivilegedAppBypass() throws Exception {
+        mFlexibilityController.onSystemServicesReady();
+
+        final String carrier1Pkg1 = "com.test.carrier.1.pkg.1";
+        final String carrier1Pkg2 = "com.test.carrier.1.pkg.2";
+        final String carrier2Pkg = "com.test.carrier.2.pkg";
+        final String nonCarrierPkg = "com.test.normal.pkg";
+
+        setPackageUid(carrier1Pkg1, 1);
+        setPackageUid(carrier1Pkg2, 11);
+        setPackageUid(carrier2Pkg, 2);
+        setPackageUid(nonCarrierPkg, 3);
+
+        // Set the second carrier's privileged list before SIM configuration is sent to test
+        // initialization.
+        setCarrierPrivilegedAppList(2, carrier2Pkg);
+
+        UiccSlotMapping sim1 = mock(UiccSlotMapping.class);
+        UiccSlotMapping sim2 = mock(UiccSlotMapping.class);
+        doReturn(1).when(sim1).getLogicalSlotIndex();
+        doReturn(2).when(sim2).getLogicalSlotIndex();
+        setSimSlotMappings(List.of(sim1, sim2));
+
+        JobStatus jsHighC1P1 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier1Pkg1);
+        JobStatus jsDefaultC1P1 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier1Pkg1);
+        JobStatus jsLowC1P1 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier1Pkg1);
+        JobStatus jsMinC1P1 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier1Pkg1);
+        JobStatus jsHighC1P2 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier1Pkg2);
+        JobStatus jsDefaultC1P2 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier1Pkg2);
+        JobStatus jsLowC1P2 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier1Pkg2);
+        JobStatus jsMinC1P2 = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier1Pkg2);
+        JobStatus jsHighC2P = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier2Pkg);
+        JobStatus jsDefaultC2P = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier2Pkg);
+        JobStatus jsLowC2P = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier2Pkg);
+        JobStatus jsMinC2P = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier2Pkg);
+        JobStatus jsHighNCP = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH), nonCarrierPkg);
+        JobStatus jsDefaultNCP = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), nonCarrierPkg);
+        JobStatus jsLowNCP = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW), nonCarrierPkg);
+        JobStatus jsMinNCP = createJobStatus("testCarrierPrivilegedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN), nonCarrierPkg);
+
+        setCarrierPrivilegedAppList(1);
+        synchronized (mFlexibilityController.mLock) {
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP));
+        }
+
+        // Only mark the first package of carrier 1 as privileged. Only that app's jobs should
+        // be exempted.
+        setCarrierPrivilegedAppList(1, carrier1Pkg1);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP));
+        }
+
+        // Add the second package of carrier 1. Both apps' jobs should be exempted.
+        setCarrierPrivilegedAppList(1, carrier1Pkg1, carrier1Pkg2);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP));
+        }
+
+        // Remove a SIM slot. The relevant app's should no longer have exempted jobs.
+        setSimSlotMappings(List.of(sim1));
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP));
+        }
+    }
+
+    @Test
     public void testForegroundAppBypass() {
         JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
                 createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
@@ -1753,6 +1921,24 @@
         }
     }
 
+    private void setCarrierPrivilegedAppList(int logicalIndex, String... packages) {
+        final ArraySet<String> packageSet = packages == null
+                ? new ArraySet<>() : new ArraySet<>(packages);
+        mCarrierPrivilegedApps.put(logicalIndex, packageSet);
+
+        TelephonyManager.CarrierPrivilegesCallback callback =
+                mCarrierPrivilegedCallbacks.get(logicalIndex);
+        if (callback != null) {
+            callback.onCarrierPrivilegesChanged(packageSet, Collections.emptySet());
+            waitForQuietModuleThread();
+        }
+    }
+
+    private void setPackageUid(final String pkgName, final int uid) throws Exception {
+        doReturn(uid).when(mIPackageManager)
+                .getPackageUid(eq(pkgName), anyLong(), eq(UserHandle.getUserId(uid)));
+    }
+
     private void setPowerWhitelistExceptIdle(String... packages) {
         doReturn(packages == null ? EmptyArray.STRING : packages)
                 .when(mDeviceIdleInternal).getFullPowerWhitelistExceptIdle();
@@ -1763,6 +1949,47 @@
         }
     }
 
+    private void setSimSlotMappings(@Nullable Collection<UiccSlotMapping> simSlotMapping) {
+        clearInvocations(mTelephonyManager);
+        final Collection<UiccSlotMapping> returnedMapping = simSlotMapping == null
+                ? Collections.emptyList() : simSlotMapping;
+        doReturn(returnedMapping).when(mTelephonyManager).getSimSlotMapping();
+        if (mBroadcastReceiver != null) {
+            final Intent intent = new Intent(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
+            mBroadcastReceiver.onReceive(mContext, intent);
+            waitForQuietModuleThread();
+        }
+        if (returnedMapping.size() > 0) {
+            ArgumentCaptor<TelephonyManager.CarrierPrivilegesCallback> callbackCaptor =
+                    ArgumentCaptor.forClass(TelephonyManager.CarrierPrivilegesCallback.class);
+            ArgumentCaptor<Integer> logicalIndexCaptor = ArgumentCaptor.forClass(Integer.class);
+
+            final int minExpectedNewRegistrations = Math.max(0,
+                    returnedMapping.size() - mCarrierPrivilegedCallbacks.size());
+            verify(mTelephonyManager, atLeast(minExpectedNewRegistrations))
+                    .registerCarrierPrivilegesCallback(
+                            logicalIndexCaptor.capture(), any(), callbackCaptor.capture());
+
+            final List<Integer> registeredIndices = logicalIndexCaptor.getAllValues();
+            final List<TelephonyManager.CarrierPrivilegesCallback> registeredCallbacks =
+                    callbackCaptor.getAllValues();
+            for (int i = 0; i < registeredIndices.size(); ++i) {
+                final int logicalIndex = registeredIndices.get(i);
+                final TelephonyManager.CarrierPrivilegesCallback callback =
+                        registeredCallbacks.get(i);
+
+                mCarrierPrivilegedCallbacks.put(logicalIndex, callback);
+
+                // The API contract promises a callback upon registration with the current list.
+                final ArraySet<String> cpApps = mCarrierPrivilegedApps.get(logicalIndex);
+                callback.onCarrierPrivilegesChanged(
+                        cpApps == null ? Collections.emptySet() : cpApps,
+                        Collections.emptySet());
+            }
+            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/utils/com/android/server/testutils/StubTransaction.java b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
index b9ece93..e27bb4c 100644
--- a/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
+++ b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java
@@ -40,19 +40,12 @@
 public class StubTransaction extends SurfaceControl.Transaction {
 
     private HashSet<Runnable> mWindowInfosReportedListeners = new HashSet<>();
-    private HashSet<SurfaceControl.TransactionCommittedListener> mTransactionCommittedListeners =
-            new HashSet<>();
 
     @Override
     public void apply() {
         for (Runnable listener : mWindowInfosReportedListeners) {
             listener.run();
         }
-        for (SurfaceControl.TransactionCommittedListener listener
-                : mTransactionCommittedListeners) {
-            listener.onTransactionCommitted();
-        }
-        mTransactionCommittedListeners.clear();
     }
 
     @Override
@@ -246,9 +239,6 @@
     @Override
     public SurfaceControl.Transaction addTransactionCommittedListener(Executor executor,
             SurfaceControl.TransactionCommittedListener listener) {
-        SurfaceControl.TransactionCommittedListener listenerInner =
-                () -> executor.execute(listener::onTransactionCommitted);
-        mTransactionCommittedListeners.add(listenerInner);
         return this;
     }
 
diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp
index 2f29d10..515898a 100644
--- a/services/tests/uiservicestests/Android.bp
+++ b/services/tests/uiservicestests/Android.bp
@@ -48,6 +48,8 @@
         "notification_flags_lib",
         "platform-test-rules",
         "SettingsLib",
+        "libprotobuf-java-lite",
+        "platformprotoslite",
     ],
 
     libs: [
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 4dded1d..05b6c90 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -37,6 +37,7 @@
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyLong;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -885,6 +886,7 @@
             return true;
         });
 
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -915,6 +917,7 @@
             return true;
         });
 
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -945,6 +948,7 @@
             return true;
         });
 
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a/a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -975,6 +979,7 @@
             return true;
         });
 
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a/a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -1152,6 +1157,58 @@
     }
 
     @Test
+    public void testUpgradeAppNoPermissionNoRebind() throws Exception {
+        Context context = spy(getContext());
+        doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles,
+                mIpm,
+                APPROVAL_BY_COMPONENT);
+
+        List<String> packages = new ArrayList<>();
+        packages.add("package");
+        addExpectedServices(service, packages, 0);
+
+        final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1");
+        final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2");
+
+        // Both components are approved initially
+        mExpectedPrimaryComponentNames.clear();
+        mExpectedPrimaryPackages.clear();
+        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+        mExpectedSecondaryComponentNames.clear();
+        mExpectedSecondaryPackages.clear();
+
+        loadXml(service);
+
+        //Component package/C1 loses bind permission
+        when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
+                (Answer<ServiceInfo>) invocation -> {
+                    ComponentName invocationCn = invocation.getArgument(0);
+                    if (invocationCn != null) {
+                        ServiceInfo serviceInfo = new ServiceInfo();
+                        serviceInfo.packageName = invocationCn.getPackageName();
+                        serviceInfo.name = invocationCn.getClassName();
+                        if (invocationCn.equals(unapprovedComponent)) {
+                            serviceInfo.permission = "none";
+                        } else {
+                            serviceInfo.permission = service.getConfig().bindPermission;
+                        }
+                        serviceInfo.metaData = null;
+                        return serviceInfo;
+                    }
+                    return null;
+                }
+        );
+
+        // Trigger package update
+        service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+        assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent));
+        assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent));
+    }
+
+    @Test
     public void testSetPackageOrComponentEnabled() throws Exception {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
             ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 78fb570..bfc47fd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -324,7 +324,11 @@
         when(mPermissionHelper.getNotificationPermissionValues(USER_SYSTEM))
                 .thenReturn(appPermissions);
 
-        when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0}));
+        IntArray currentProfileIds = IntArray.wrap(new int[]{0});
+        if (UserManager.isHeadlessSystemUserMode()) {
+            currentProfileIds.add(UserHandle.getUserId(UID_HEADLESS));
+        }
+        when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds);
 
         mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java
new file mode 100644
index 0000000..f724510
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.provider.Settings;
+import android.service.notification.ZenPolicy;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.os.dnd.ActiveRuleType;
+import com.android.os.dnd.ChannelPolicy;
+import com.android.os.dnd.ConversationType;
+import com.android.os.dnd.PeopleType;
+import com.android.os.dnd.State;
+import com.android.os.dnd.ZenMode;
+
+import com.google.protobuf.Internal;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Test to validate that logging enums used in Zen classes match their API definitions. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ZenEnumTest {
+
+    @Test
+    public void testEnum_zenMode() {
+        testEnum(Settings.Global.class, "ZEN_MODE", ZenMode.class, "ZEN_MODE");
+    }
+
+    @Test
+    public void testEnum_activeRuleType() {
+        testEnum(AutomaticZenRule.class, "TYPE", ActiveRuleType.class, "TYPE");
+    }
+
+    @Test
+    public void testEnum_zenPolicyState() {
+        testEnum(ZenPolicy.class, "STATE", State.class, "STATE");
+    }
+
+    @Test
+    public void testEnum_zenPolicyChannelPolicy() {
+        testEnum(ZenPolicy.class, "CHANNEL_POLICY", ChannelPolicy.class, "CHANNEL_POLICY");
+    }
+
+    @Test
+    public void testEnum_zenPolicyConversationType() {
+        testEnum(ZenPolicy.class, "CONVERSATION_SENDERS", ConversationType.class, "CONV");
+    }
+
+    @Test
+    public void testEnum_zenPolicyPeopleType() {
+        testEnum(ZenPolicy.class, "PEOPLE_TYPE", PeopleType.class, "PEOPLE");
+    }
+
+    /**
+     * Verifies that any constants (i.e. {@code public static final int} fields) named {@code
+     * <apiPrefix>_SOMETHING} in {@code apiClass} are present and have the same numerical value
+     * in the enum values defined in {@code loggingProtoEnumClass}.
+     *
+     * <p>Note that <em>extra</em> values in the logging enum are accepted (since we have one of
+     * those, and the main goal of this test is that we don't forget to update the logging enum
+     * if new API enum values are added).
+     */
+    private static void testEnum(Class<?> apiClass, String apiPrefix,
+            Class<? extends Internal.EnumLite> loggingProtoEnumClass,
+            String loggingPrefix) {
+        Map<String, Integer> apiConstants =
+                Arrays.stream(apiClass.getDeclaredFields())
+                        .filter(f -> Modifier.isPublic(f.getModifiers()))
+                        .filter(f -> Modifier.isStatic(f.getModifiers()))
+                        .filter(f -> Modifier.isFinal(f.getModifiers()))
+                        .filter(f -> f.getType().equals(int.class))
+                        .filter(f -> f.getName().startsWith(apiPrefix + "_"))
+                        .collect(Collectors.toMap(
+                                Field::getName,
+                                ZenEnumTest::getStaticFieldIntValue));
+
+        Map<String, Integer> loggingConstants =
+                Arrays.stream(loggingProtoEnumClass.getEnumConstants())
+                        .collect(Collectors.toMap(
+                                v -> v.toString(),
+                                v -> v.getNumber()));
+
+        Map<String, Integer> renamedApiConstants = apiConstants.entrySet().stream()
+                .collect(Collectors.toMap(
+                        entry -> entry.getKey().replace(apiPrefix + "_", loggingPrefix + "_"),
+                        Map.Entry::getValue));
+
+        assertThat(loggingConstants).containsAtLeastEntriesIn(renamedApiConstants);
+    }
+
+    private static int getStaticFieldIntValue(Field f) {
+        try {
+            return f.getInt(null);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/GroupedAggregatedLogRecordsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/GroupedAggregatedLogRecordsTest.java
index 038f1db..54ab367 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/GroupedAggregatedLogRecordsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/GroupedAggregatedLogRecordsTest.java
@@ -135,7 +135,7 @@
         AggregatedLogRecord<TestSingleLogRecord> droppedRecord =
                 records.add(createRecord(GROUP_1, KEY_2, createTime++));
         assertThat(droppedRecord).isNotNull();
-        assertThat(droppedRecord.getLatest()).isEqualTo(mTestRecords.getFirst());
+        assertThat(droppedRecord.getLatest()).isEqualTo(mTestRecords.get(0));
 
         dumpRecords(records);
         assertGroupHeadersWrittenOnce(records, GROUP_1);
@@ -155,7 +155,7 @@
         AggregatedLogRecord<TestSingleLogRecord> droppedRecord =
                 records.add(createRecord(GROUP_1, KEY_1, createTime + AGGREGATION_TIME_LIMIT));
         assertThat(droppedRecord).isNotNull();
-        assertThat(droppedRecord.getLatest()).isEqualTo(mTestRecords.getFirst());
+        assertThat(droppedRecord.getLatest()).isEqualTo(mTestRecords.get(0));
 
         dumpRecords(records);
         assertGroupHeadersWrittenOnce(records, GROUP_1);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 1a1fe95..0c1fbf3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -94,7 +94,6 @@
     public void setUp() throws Exception {
         assumeFalse(WindowManagerService.sEnableShellTransitions);
         mAppTransitionController = new AppTransitionController(mWm, mDisplayContent);
-        mWm.mAnimator.ready();
     }
 
     @Test
@@ -856,7 +855,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation run by the remote handler.
         assertTrue(remoteAnimationRunner.isAnimationStarted());
@@ -887,7 +886,7 @@
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity,
                 null /* changingTaskFragment */);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation is not run by the remote handler because the activity is filling the Task.
         assertFalse(remoteAnimationRunner.isAnimationStarted());
@@ -922,7 +921,7 @@
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity,
                 null /* changingTaskFragment */);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation run by the remote handler.
         assertTrue(remoteAnimationRunner.isAnimationStarted());
@@ -947,7 +946,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation run by the remote handler.
         assertTrue(remoteAnimationRunner.isAnimationStarted());
@@ -974,7 +973,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment1);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation run by the remote handler.
         assertTrue(remoteAnimationRunner.isAnimationStarted());
@@ -998,7 +997,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation not run by the remote handler.
         assertFalse(remoteAnimationRunner.isAnimationStarted());
@@ -1025,7 +1024,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation should not run by the remote handler when there are non-embedded activities of
         // different UID.
@@ -1052,7 +1051,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // Animation should not run by the remote handler when there is wallpaper in the transition.
         assertFalse(remoteAnimationRunner.isAnimationStarted());
@@ -1086,7 +1085,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity1, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // The animation will be animated remotely by client and all activities are input disabled
         // for untrusted animation.
@@ -1137,7 +1136,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // The animation will be animated remotely by client and all activities are input disabled
         // for untrusted animation.
@@ -1179,7 +1178,7 @@
 
         // Prepare and start transition.
         prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
         // The animation will be animated remotely by client, but input should not be dropped for
         // fully trusted.
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index 1f15ec3..2085d61 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -371,7 +371,6 @@
         mDisplayContent.getInsetsPolicy().updateBarControlTarget(app);
         mDisplayContent.getInsetsPolicy().showTransient(statusBars(),
                 true /* isGestureOnSystemBar */);
-        mWm.mAnimator.ready();
         waitUntilWindowAnimatorIdle();
 
         assertTrue(mDisplayContent.getInsetsPolicy().isTransient(statusBars()));
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
index a163801..11d9629 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -43,6 +43,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -112,7 +113,6 @@
         runWithScissors(mWm.mH, () -> mHandler = new TestHandler(null, mClock), 0);
         mController = new RemoteAnimationController(mWm, mDisplayContent, mAdapter,
                 mHandler, false /*isActivityEmbedding*/);
-        mWm.mAnimator.ready();
     }
 
     private WindowState createAppOverlayWindow() {
@@ -136,7 +136,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -168,7 +168,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -290,7 +290,7 @@
         adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                 mFinishedCallback);
         mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
         final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                 ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
         final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -336,7 +336,7 @@
         task.applyAnimationUnchecked(null /* lp */, true /* enter */, TRANSIT_OLD_TASK_OPEN,
                 false /* isVoiceInteraction */, null /* sources */);
         mController.goodToGo(TRANSIT_OLD_TASK_OPEN);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
         final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                 ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
         try {
@@ -363,7 +363,7 @@
             ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash,
                     mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -417,7 +417,7 @@
             ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash,
                     mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -471,7 +471,7 @@
             ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash,
                     mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -526,7 +526,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -559,7 +559,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -595,7 +595,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_KEYGUARD_GOING_AWAY);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -645,7 +645,7 @@
             adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                     mFinishedCallback);
             mController.goodToGo(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
             final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
                     ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
             final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor =
@@ -782,7 +782,7 @@
 
             mDisplayContent.applySurfaceChangesTransaction();
             mController.goodToGo(TRANSIT_OLD_TASK_OPEN);
-            waitUntilWindowAnimatorIdle();
+            mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
 
             verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_OPEN),
                     any(), any(), any(), any());
@@ -810,7 +810,7 @@
         adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION,
                 mFinishedCallback);
         mController.goodToGo(transit);
-        waitUntilWindowAnimatorIdle();
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
         return adapter;
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 7ab093d..a8f6fe8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -546,7 +546,7 @@
         // This makes sure all previous messages in the handler are fully processed vs. just popping
         // them from the message queue.
         final AtomicBoolean currentMessagesProcessed = new AtomicBoolean(false);
-        wm.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
+        wm.mAnimator.getChoreographer().postFrameCallback(time -> {
             synchronized (currentMessagesProcessed) {
                 currentMessagesProcessed.set(true);
                 currentMessagesProcessed.notifyAll();
diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java
index b84ff29..12e04c2 100644
--- a/telephony/java/android/telephony/ims/ImsService.java
+++ b/telephony/java/android/telephony/ims/ImsService.java
@@ -195,7 +195,8 @@
                     // whether or not ImsFeature.FEATURE_EMERGENCY_MMTEL feature is set and should
                     // not be set by users of ImsService.
                     CAPABILITY_SIP_DELEGATE_CREATION,
-                    CAPABILITY_TERMINAL_BASED_CALL_WAITING
+                    CAPABILITY_TERMINAL_BASED_CALL_WAITING,
+                    CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ImsServiceCapability {}
@@ -206,7 +207,9 @@
      */
     private static final Map<Long, String> CAPABILITIES_LOG_MAP = Map.of(
             CAPABILITY_EMERGENCY_OVER_MMTEL, "EMERGENCY_OVER_MMTEL",
-            CAPABILITY_SIP_DELEGATE_CREATION, "SIP_DELEGATE_CREATION");
+            CAPABILITY_SIP_DELEGATE_CREATION, "SIP_DELEGATE_CREATION",
+            CAPABILITY_TERMINAL_BASED_CALL_WAITING, "TERMINAL_BASED_CALL_WAITING",
+            CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING, "SIMULTANEOUS_CALLING");
 
     /**
      * The intent that must be defined as an intent-filter in the AndroidManifest of the ImsService.
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index 1d71f95..d658d59 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -63,17 +63,20 @@
     ],
 }
 
-android_library_import {
-    name: "wm-flicker-window-extensions_nodeps",
-    aars: ["libs/window-extensions-release.aar"],
-    sdk_version: "current",
-}
-
 java_library {
     name: "wm-flicker-window-extensions",
     sdk_version: "current",
     static_libs: [
-        "wm-flicker-window-extensions_nodeps",
+        "androidx.window.extensions_extensions-nodeps",
+    ],
+    installable: false,
+}
+
+java_library {
+    name: "wm-flicker-window-extensions-core",
+    sdk_version: "current",
+    static_libs: [
+        "androidx.window.extensions.core_core-nodeps",
     ],
     installable: false,
 }
diff --git a/tests/FlickerTests/libs/window-extensions-release.aar b/tests/FlickerTests/libs/window-extensions-release.aar
deleted file mode 100644
index 918e514..0000000
--- a/tests/FlickerTests/libs/window-extensions-release.aar
+++ /dev/null
Binary files differ
diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
index a64996c..d9a4c26 100644
--- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
@@ -59,6 +59,7 @@
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.util.LinkedList;
+import java.util.TreeMap;
 
 /**
  * Test class for {@link ProtoLogImpl}.
@@ -89,7 +90,7 @@
         //noinspection ResultOfMethodCallIgnored
         mFile.delete();
         mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename,
-                1024 * 1024, mReader, 1024);
+                1024 * 1024, mReader, 1024, new TreeMap<>());
     }
 
     @After
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index 270f595..548adef 100644
--- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -64,6 +64,7 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Random;
+import java.util.TreeMap;
 
 import perfetto.protos.Protolog;
 import perfetto.protos.ProtologCommon;
@@ -152,7 +153,8 @@
                 .thenAnswer(it -> new ProtoInputStream(mViewerConfigBuilder.build().toByteArray()));
 
         mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
-        mProtoLog = new PerfettoProtoLogImpl(viewerConfigInputStreamProvider, mReader);
+        mProtoLog =
+                new PerfettoProtoLogImpl(viewerConfigInputStreamProvider, mReader, new TreeMap<>());
     }
 
     @After
diff --git a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
index a359155..2690bc5 100644
--- a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
@@ -25,6 +25,7 @@
         const val READ_LOG_CMD = "read-log"
         private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD)
 
+        // TODO: This is always the same. I don't think it's required
         private const val PROTOLOG_CLASS_PARAM = "--protolog-class"
         private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class"
         private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar"
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
index 1381847..837dae9 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
@@ -24,11 +24,17 @@
 import com.github.javaparser.ParserConfiguration
 import com.github.javaparser.StaticJavaParser
 import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.NodeList
 import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.InitializerDeclaration
+import com.github.javaparser.ast.expr.FieldAccessExpr
 import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NameExpr
 import com.github.javaparser.ast.expr.NullLiteralExpr
+import com.github.javaparser.ast.expr.ObjectCreationExpr
 import com.github.javaparser.ast.expr.SimpleName
 import com.github.javaparser.ast.expr.StringLiteralExpr
+import com.github.javaparser.ast.stmt.BlockStmt
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileNotFoundException
@@ -39,8 +45,7 @@
 import java.util.concurrent.Executors
 import java.util.jar.JarOutputStream
 import java.util.zip.ZipEntry
-import kotlin.math.abs
-import kotlin.random.Random
+import kotlin.math.absoluteValue
 import kotlin.system.exitProcess
 
 object ProtoLogTool {
@@ -72,7 +77,11 @@
     }
 
     private fun processClasses(command: CommandOptions) {
-        val generationHash = abs(Random.nextInt())
+        // A deterministic hash based on the group jar path and the source files we are processing.
+        // The hash is required to make sure different ProtoLogImpls don't conflict.
+        val generationHash = (command.javaSourceArgs.toTypedArray() + command.protoLogGroupsJarArg)
+                .contentHashCode().absoluteValue
+
         // Need to generate a new impl class to inject static constants into the class.
         val generatedProtoLogImplClass =
             "com.android.internal.protolog.ProtoLogImpl_$generationHash"
@@ -93,7 +102,8 @@
         outJar.putNextEntry(zipEntry(protologImplPath))
 
         outJar.write(generateProtoLogImpl(protologImplName, command.viewerConfigFilePathArg,
-            command.legacyViewerConfigFilePathArg, command.legacyOutputFilePath).toByteArray())
+            command.legacyViewerConfigFilePathArg, command.legacyOutputFilePath,
+            groups, command.protoLogGroupsClassNameArg).toByteArray())
 
         val executor = newThreadPool()
 
@@ -137,6 +147,8 @@
         viewerConfigFilePath: String,
         legacyViewerConfigFilePath: String?,
         legacyOutputFilePath: String?,
+        groups: Map<String, LogGroup>,
+        protoLogGroupsClassName: String,
     ): String {
         val file = File(PROTOLOG_IMPL_SRC_PATH)
 
@@ -157,7 +169,8 @@
         classNameNode.setId(protoLogImplGenName)
 
         injectConstants(classDeclaration,
-            viewerConfigFilePath, legacyViewerConfigFilePath, legacyOutputFilePath)
+            viewerConfigFilePath, legacyViewerConfigFilePath, legacyOutputFilePath, groups,
+            protoLogGroupsClassName)
 
         return code.toString()
     }
@@ -166,7 +179,9 @@
         classDeclaration: ClassOrInterfaceDeclaration,
         viewerConfigFilePath: String,
         legacyViewerConfigFilePath: String?,
-        legacyOutputFilePath: String?
+        legacyOutputFilePath: String?,
+        groups: Map<String, LogGroup>,
+        protoLogGroupsClassName: String
     ) {
         classDeclaration.fields.forEach { field ->
             field.getAnnotationByClass(ProtoLogToolInjected::class.java)
@@ -194,6 +209,35 @@
                                                 StringLiteralExpr(it)
                                             } ?: NullLiteralExpr())
                                 }
+                                ProtoLogToolInjected.Value.LOG_GROUPS.name -> {
+                                    val initializerBlockStmt = BlockStmt()
+                                    for (group in groups) {
+                                        initializerBlockStmt.addStatement(
+                                            MethodCallExpr()
+                                                    .setName("put")
+                                                    .setArguments(
+                                                        NodeList(StringLiteralExpr(group.key),
+                                                            FieldAccessExpr()
+                                                                    .setScope(
+                                                                        NameExpr(
+                                                                            protoLogGroupsClassName
+                                                                        ))
+                                                                    .setName(group.value.name)))
+                                        )
+                                        group.key
+                                    }
+
+                                    val treeMapCreation = ObjectCreationExpr()
+                                            .setType("TreeMap<String, IProtoLogGroup>")
+                                            .setAnonymousClassBody(NodeList(
+                                                InitializerDeclaration().setBody(
+                                                    initializerBlockStmt
+                                                )
+                                            ))
+
+                                    field.setFinal(true)
+                                    field.variables.first().setInitializer(treeMapCreation)
+                                }
                                 else -> error("Unhandled ProtoLogToolInjected value: $valueName.")
                             }
                         }
diff --git a/tools/streaming_proto/Android.bp b/tools/streaming_proto/Android.bp
index b18bdff..b1b314fc 100644
--- a/tools/streaming_proto/Android.bp
+++ b/tools/streaming_proto/Android.bp
@@ -17,6 +17,7 @@
 // ==========================================================
 // Build the host executable: protoc-gen-javastream
 // ==========================================================
+
 package {
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
@@ -41,6 +42,32 @@
     static_libs: ["libprotoc"],
 }
 
+// ==========================================================
+// Build the host static library: java_streaming_proto_lib
+// ==========================================================
+
+cc_library_host_static {
+    name: "java_streaming_proto_lib",
+    defaults: ["protoc-gen-stream-defaults"],
+    target: {
+        darwin: {
+            cflags: ["-D_DARWIN_UNLIMITED_STREAMS"],
+        },
+    },
+    cflags: [
+        "-Wno-format-y2k",
+        "-DSTATIC_ANDROIDFW_FOR_TOOLS",
+    ],
+
+    srcs: [
+        "java/java_proto_stream_code_generator.cpp",
+    ],
+}
+
+// ==========================================================
+// Build the host executable: protoc-gen-javastream
+// ==========================================================
+
 cc_binary_host {
     name: "protoc-gen-javastream",
     srcs: [
@@ -48,8 +75,13 @@
     ],
 
     defaults: ["protoc-gen-stream-defaults"],
+    static_libs: ["java_streaming_proto_lib"],
 }
 
+// ==========================================================
+// Build the host executable: protoc-gen-cppstream
+// ==========================================================
+
 cc_binary_host {
     name: "protoc-gen-cppstream",
     srcs: [
@@ -60,13 +92,31 @@
 }
 
 // ==========================================================
+// Build the host tests: StreamingProtoTest
+// ==========================================================
+
+cc_test_host {
+    name: "StreamingProtoTest",
+    defaults: ["protoc-gen-stream-defaults"],
+    srcs: [
+        "test/unit/**/*.cpp",
+    ],
+    static_libs: [
+        "java_streaming_proto_lib",
+        "libgmock",
+        "libgtest",
+    ],
+}
+
+// ==========================================================
 // Build the java test
 // ==========================================================
+
 java_library {
-    name: "StreamingProtoTest",
+    name: "StreamingProtoJavaIntegrationTest",
     srcs: [
-        "test/**/*.java",
-        "test/**/*.proto",
+        "test/integration/**/*.java",
+        "test/integration/**/*.proto",
     ],
     proto: {
         type: "stream",
diff --git a/tools/streaming_proto/java/java_proto_stream_code_generator.cpp b/tools/streaming_proto/java/java_proto_stream_code_generator.cpp
new file mode 100644
index 0000000..9d61111
--- /dev/null
+++ b/tools/streaming_proto/java/java_proto_stream_code_generator.cpp
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "java_proto_stream_code_generator.h"
+
+#include <stdio.h>
+
+#include <iomanip>
+#include <iostream>
+#include <map>
+#include <sstream>
+#include <string>
+
+#include "Errors.h"
+
+using namespace android::stream_proto;
+using namespace google::protobuf::io;
+using namespace std;
+
+/**
+ * If the descriptor gives us a class name, use that. Otherwise make one up from
+ * the filename of the .proto file.
+ */
+static string make_outer_class_name(const FileDescriptorProto& file_descriptor) {
+    string name = file_descriptor.options().java_outer_classname();
+    if (name.size() == 0) {
+        name = to_camel_case(file_base_name(file_descriptor.name()));
+        if (name.size() == 0) {
+            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE,
+                       "Unable to make an outer class name for file: %s",
+                       file_descriptor.name().c_str());
+            name = "Unknown";
+        }
+    }
+    return name;
+}
+
+/**
+ * Figure out the package name that we are generating.
+ */
+static string make_java_package(const FileDescriptorProto& file_descriptor) {
+    if (file_descriptor.options().has_java_package()) {
+        return file_descriptor.options().java_package();
+    } else {
+        return file_descriptor.package();
+    }
+}
+
+/**
+ * Figure out the name of the file we are generating.
+ */
+static string make_file_name(const FileDescriptorProto& file_descriptor, const string& class_name) {
+    string const package = make_java_package(file_descriptor);
+    string result;
+    if (package.size() > 0) {
+        result = replace_string(package, '.', '/');
+        result += '/';
+    }
+
+    result += class_name;
+    result += ".java";
+
+    return result;
+}
+
+static string indent_more(const string& indent) {
+    return indent + INDENT;
+}
+
+/**
+ * Write the constants for an enum.
+ */
+static void write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& indent) {
+    const int N = enu.value_size();
+    text << indent << "// enum " << enu.name() << endl;
+    for (int i = 0; i < N; i++) {
+        const EnumValueDescriptorProto& value = enu.value(i);
+        text << indent << "public static final int " << make_constant_name(value.name()) << " = "
+             << value.number() << ";" << endl;
+    }
+    text << endl;
+}
+
+/**
+ * Write a field.
+ */
+static void write_field(stringstream& text, const FieldDescriptorProto& field,
+                        const string& indent) {
+    string optional_comment =
+            field.label() == FieldDescriptorProto::LABEL_OPTIONAL ? "optional " : "";
+    string repeated_comment =
+            field.label() == FieldDescriptorProto::LABEL_REPEATED ? "repeated " : "";
+    string proto_type = get_proto_type(field);
+    string packed_comment = field.options().packed() ? " [packed=true]" : "";
+    text << indent << "// " << optional_comment << repeated_comment << proto_type << ' '
+         << field.name() << " = " << field.number() << packed_comment << ';' << endl;
+
+    text << indent << "public static final long " << make_constant_name(field.name()) << " = 0x";
+
+    ios::fmtflags fmt(text.flags());
+    text << setfill('0') << setw(16) << hex << get_field_id(field);
+    text.flags(fmt);
+
+    text << "L;" << endl;
+
+    text << endl;
+}
+
+/**
+ * Write a Message constants class.
+ */
+static void write_message(stringstream& text, const DescriptorProto& message,
+                          const string& indent) {
+    int N;
+    const string indented = indent_more(indent);
+
+    text << indent << "// message " << message.name() << endl;
+    text << indent << "public final class " << message.name() << " {" << endl;
+    text << endl;
+
+    // Enums
+    N = message.enum_type_size();
+    for (int i = 0; i < N; i++) {
+        write_enum(text, message.enum_type(i), indented);
+    }
+
+    // Nested classes
+    N = message.nested_type_size();
+    for (int i = 0; i < N; i++) {
+        write_message(text, message.nested_type(i), indented);
+    }
+
+    // Fields
+    N = message.field_size();
+    for (int i = 0; i < N; i++) {
+        write_field(text, message.field(i), indented);
+    }
+
+    text << indent << "}" << endl;
+    text << endl;
+}
+
+/**
+ * Write the contents of a file.
+ *
+ * If there are enums and generate_outer is false, invalid java code will be generated.
+ */
+static void write_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor,
+                       const string& filename, bool generate_outer,
+                       const vector<EnumDescriptorProto>& enums,
+                       const vector<DescriptorProto>& messages) {
+    stringstream text;
+
+    string const package_name = make_java_package(file_descriptor);
+    string const outer_class_name = make_outer_class_name(file_descriptor);
+
+    text << "// Generated by protoc-gen-javastream. DO NOT MODIFY." << endl;
+    text << "// source: " << file_descriptor.name() << endl << endl;
+
+    if (package_name.size() > 0) {
+        if (package_name.size() > 0) {
+            text << "package " << package_name << ";" << endl;
+            text << endl;
+        }
+    }
+
+    // This bit of policy is android api rules specific: Raw proto classes
+    // must never be in the API
+    text << "/** @hide */" << endl;
+    //    text << "@android.annotation.TestApi" << endl;
+
+    if (generate_outer) {
+        text << "public final class " << outer_class_name << " {" << endl;
+        text << endl;
+    }
+
+    size_t N;
+    const string indented = generate_outer ? indent_more("") : string();
+
+    N = enums.size();
+    for (size_t i = 0; i < N; i++) {
+        write_enum(text, enums[i], indented);
+    }
+
+    N = messages.size();
+    for (size_t i = 0; i < N; i++) {
+        write_message(text, messages[i], indented);
+    }
+
+    if (generate_outer) {
+        text << "}" << endl;
+    }
+
+    CodeGeneratorResponse::File* file_response = response->add_file();
+    file_response->set_name(filename);
+    file_response->set_content(text.str());
+}
+
+/**
+ * Write one file per class.  Put all of the enums into the "outer" class.
+ */
+static void write_multiple_files(CodeGeneratorResponse* response,
+                                 const FileDescriptorProto& file_descriptor,
+                                 set<string> messages_to_compile) {
+    // If there is anything to put in the outer class file, create one
+    if (file_descriptor.enum_type_size() > 0) {
+        vector<EnumDescriptorProto> enums;
+        int N = file_descriptor.enum_type_size();
+        for (int i = 0; i < N; i++) {
+            auto enum_full_name =
+                    file_descriptor.package() + "." + file_descriptor.enum_type(i).name();
+            if (!messages_to_compile.empty() && !messages_to_compile.count(enum_full_name)) {
+                continue;
+            }
+            enums.push_back(file_descriptor.enum_type(i));
+        }
+
+        vector<DescriptorProto> messages;
+
+        if (messages_to_compile.empty() || !enums.empty()) {
+            write_file(response, file_descriptor,
+                       make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
+                       true, enums, messages);
+        }
+    }
+
+    // For each of the message types, make a file
+    int N = file_descriptor.message_type_size();
+    for (int i = 0; i < N; i++) {
+        vector<EnumDescriptorProto> enums;
+
+        vector<DescriptorProto> messages;
+
+        auto message_full_name =
+                file_descriptor.package() + "." + file_descriptor.message_type(i).name();
+        if (!messages_to_compile.empty() && !messages_to_compile.count(message_full_name)) {
+            continue;
+        }
+        messages.push_back(file_descriptor.message_type(i));
+
+        if (messages_to_compile.empty() || !messages.empty()) {
+            write_file(response, file_descriptor,
+                       make_file_name(file_descriptor, file_descriptor.message_type(i).name()),
+                       false, enums, messages);
+        }
+    }
+}
+
+static void write_single_file(CodeGeneratorResponse* response,
+                              const FileDescriptorProto& file_descriptor,
+                              set<string> messages_to_compile) {
+    int N;
+
+    vector<EnumDescriptorProto> enums;
+    N = file_descriptor.enum_type_size();
+    for (int i = 0; i < N; i++) {
+        auto enum_full_name = file_descriptor.package() + "." + file_descriptor.enum_type(i).name();
+        if (!messages_to_compile.empty() && !messages_to_compile.count(enum_full_name)) {
+            continue;
+        }
+
+        enums.push_back(file_descriptor.enum_type(i));
+    }
+
+    vector<DescriptorProto> messages;
+    N = file_descriptor.message_type_size();
+    for (int i = 0; i < N; i++) {
+        auto message_full_name =
+                file_descriptor.package() + "." + file_descriptor.message_type(i).name();
+
+        if (!messages_to_compile.empty() && !messages_to_compile.count(message_full_name)) {
+            continue;
+        }
+
+        messages.push_back(file_descriptor.message_type(i));
+    }
+
+    if (messages_to_compile.empty() || !enums.empty() || !messages.empty()) {
+        write_file(response, file_descriptor,
+                   make_file_name(file_descriptor, make_outer_class_name(file_descriptor)), true,
+                   enums, messages);
+    }
+}
+
+static void parse_args_string(stringstream args_string_stream,
+                              set<string>* messages_to_compile_out) {
+    string line;
+    while (getline(args_string_stream, line, ';')) {
+        stringstream line_ss(line);
+        string arg_name;
+        getline(line_ss, arg_name, ':');
+        if (arg_name == "include_filter") {
+            string full_message_name;
+            while (getline(line_ss, full_message_name, ',')) {
+                messages_to_compile_out->insert(full_message_name);
+            }
+        } else {
+            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE, "Unexpected argument '%s'.", arg_name.c_str());
+        }
+    }
+}
+
+CodeGeneratorResponse generate_java_protostream_code(CodeGeneratorRequest request) {
+    CodeGeneratorResponse response;
+
+    set<string> messages_to_compile;
+    auto request_params = request.parameter();
+    if (!request_params.empty()) {
+        parse_args_string(stringstream(request_params), &messages_to_compile);
+    }
+
+    // Build the files we need.
+    const int N = request.proto_file_size();
+    for (int i = 0; i < N; i++) {
+        const FileDescriptorProto& file_descriptor = request.proto_file(i);
+        if (should_generate_for_file(request, file_descriptor.name())) {
+            if (file_descriptor.options().java_multiple_files()) {
+                write_multiple_files(&response, file_descriptor, messages_to_compile);
+            } else {
+                write_single_file(&response, file_descriptor, messages_to_compile);
+            }
+        }
+    }
+
+    return response;
+}
diff --git a/tools/streaming_proto/java/java_proto_stream_code_generator.h b/tools/streaming_proto/java/java_proto_stream_code_generator.h
new file mode 100644
index 0000000..d2492f7
--- /dev/null
+++ b/tools/streaming_proto/java/java_proto_stream_code_generator.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
+#define AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
+
+#include "stream_proto_utils.h"
+#include "string_utils.h"
+
+using namespace android::stream_proto;
+using namespace google::protobuf::io;
+using namespace std;
+
+CodeGeneratorResponse generate_java_protostream_code(CodeGeneratorRequest request);
+
+#endif // AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
\ No newline at end of file
diff --git a/tools/streaming_proto/java/main.cpp b/tools/streaming_proto/java/main.cpp
index c9c50a5..5b35504 100644
--- a/tools/streaming_proto/java/main.cpp
+++ b/tools/streaming_proto/java/main.cpp
@@ -1,268 +1,21 @@
-#include "Errors.h"
-#include "stream_proto_utils.h"
-#include "string_utils.h"
-
 #include <stdio.h>
+
 #include <iomanip>
 #include <iostream>
-#include <sstream>
 #include <map>
+#include <sstream>
+#include <string>
+
+#include "Errors.h"
+#include "java_proto_stream_code_generator.h"
+#include "stream_proto_utils.h"
 
 using namespace android::stream_proto;
 using namespace google::protobuf::io;
 using namespace std;
 
 /**
- * If the descriptor gives us a class name, use that. Otherwise make one up from
- * the filename of the .proto file.
- */
-static string
-make_outer_class_name(const FileDescriptorProto& file_descriptor)
-{
-    string name = file_descriptor.options().java_outer_classname();
-    if (name.size() == 0) {
-        name = to_camel_case(file_base_name(file_descriptor.name()));
-        if (name.size() == 0) {
-            ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE,
-                    "Unable to make an outer class name for file: %s",
-                    file_descriptor.name().c_str());
-            name = "Unknown";
-        }
-    }
-    return name;
-}
-
-/**
- * Figure out the package name that we are generating.
- */
-static string
-make_java_package(const FileDescriptorProto& file_descriptor) {
-    if (file_descriptor.options().has_java_package()) {
-        return file_descriptor.options().java_package();
-    } else {
-        return file_descriptor.package();
-    }
-}
-
-/**
- * Figure out the name of the file we are generating.
- */
-static string
-make_file_name(const FileDescriptorProto& file_descriptor, const string& class_name)
-{
-    string const package = make_java_package(file_descriptor);
-    string result;
-    if (package.size() > 0) {
-        result = replace_string(package, '.', '/');
-        result += '/';
-    }
-
-    result += class_name;
-    result += ".java";
-
-    return result;
-}
-
-static string
-indent_more(const string& indent)
-{
-    return indent + INDENT;
-}
-
-/**
- * Write the constants for an enum.
- */
-static void
-write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& indent)
-{
-    const int N = enu.value_size();
-    text << indent << "// enum " << enu.name() << endl;
-    for (int i=0; i<N; i++) {
-        const EnumValueDescriptorProto& value = enu.value(i);
-        text << indent << "public static final int "
-                << make_constant_name(value.name())
-                << " = " << value.number() << ";" << endl;
-    }
-    text << endl;
-}
-
-/**
- * Write a field.
- */
-static void
-write_field(stringstream& text, const FieldDescriptorProto& field, const string& indent)
-{
-    string optional_comment = field.label() == FieldDescriptorProto::LABEL_OPTIONAL
-            ? "optional " : "";
-    string repeated_comment = field.label() == FieldDescriptorProto::LABEL_REPEATED
-            ? "repeated " : "";
-    string proto_type = get_proto_type(field);
-    string packed_comment = field.options().packed()
-            ? " [packed=true]" : "";
-    text << indent << "// " << optional_comment << repeated_comment << proto_type << ' '
-            << field.name() << " = " << field.number() << packed_comment << ';' << endl;
-
-    text << indent << "public static final long " << make_constant_name(field.name()) << " = 0x";
-
-    ios::fmtflags fmt(text.flags());
-    text << setfill('0') << setw(16) << hex << get_field_id(field);
-    text.flags(fmt);
-
-    text << "L;" << endl;
-
-    text << endl;
-}
-
-/**
- * Write a Message constants class.
- */
-static void
-write_message(stringstream& text, const DescriptorProto& message, const string& indent)
-{
-    int N;
-    const string indented = indent_more(indent);
-
-    text << indent << "// message " << message.name() << endl;
-    text << indent << "public final class " << message.name() << " {" << endl;
-    text << endl;
-
-    // Enums
-    N = message.enum_type_size();
-    for (int i=0; i<N; i++) {
-        write_enum(text, message.enum_type(i), indented);
-    }
-
-    // Nested classes
-    N = message.nested_type_size();
-    for (int i=0; i<N; i++) {
-        write_message(text, message.nested_type(i), indented);
-    }
-
-    // Fields
-    N = message.field_size();
-    for (int i=0; i<N; i++) {
-        write_field(text, message.field(i), indented);
-    }
-
-    text << indent << "}" << endl;
-    text << endl;
-}
-
-/**
- * Write the contents of a file.
  *
- * If there are enums and generate_outer is false, invalid java code will be generated.
- */
-static void
-write_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor,
-        const string& filename, bool generate_outer,
-        const vector<EnumDescriptorProto>& enums, const vector<DescriptorProto>& messages)
-{
-    stringstream text;
-
-    string const package_name = make_java_package(file_descriptor);
-    string const outer_class_name = make_outer_class_name(file_descriptor);
-
-    text << "// Generated by protoc-gen-javastream. DO NOT MODIFY." << endl;
-    text << "// source: " << file_descriptor.name() << endl << endl;
-
-    if (package_name.size() > 0) {
-        if (package_name.size() > 0) {
-            text << "package " << package_name << ";" << endl;
-            text << endl;
-        }
-    }
-
-    // This bit of policy is android api rules specific: Raw proto classes
-    // must never be in the API
-    text << "/** @hide */" << endl;
-//    text << "@android.annotation.TestApi" << endl;
-
-    if (generate_outer) {
-        text << "public final class " << outer_class_name << " {" << endl;
-        text << endl;
-    }
-
-    size_t N;
-    const string indented = generate_outer ? indent_more("") : string();
-    
-    N = enums.size();
-    for (size_t i=0; i<N; i++) {
-        write_enum(text, enums[i], indented);
-    }
-
-    N = messages.size();
-    for (size_t i=0; i<N; i++) {
-        write_message(text, messages[i], indented);
-    }
-
-    if (generate_outer) {
-        text << "}" << endl;
-    }
-
-    CodeGeneratorResponse::File* file_response = response->add_file();
-    file_response->set_name(filename);
-    file_response->set_content(text.str());
-}
-
-/**
- * Write one file per class.  Put all of the enums into the "outer" class.
- */
-static void
-write_multiple_files(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor)
-{
-    // If there is anything to put in the outer class file, create one
-    if (file_descriptor.enum_type_size() > 0) {
-        vector<EnumDescriptorProto> enums;
-        int N = file_descriptor.enum_type_size();
-        for (int i=0; i<N; i++) {
-            enums.push_back(file_descriptor.enum_type(i));
-        }
-
-        vector<DescriptorProto> messages;
-
-        write_file(response, file_descriptor,
-                make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
-                true, enums, messages);
-    }
-
-    // For each of the message types, make a file
-    int N = file_descriptor.message_type_size();
-    for (int i=0; i<N; i++) {
-        vector<EnumDescriptorProto> enums;
-
-        vector<DescriptorProto> messages;
-        messages.push_back(file_descriptor.message_type(i));
-
-        write_file(response, file_descriptor,
-                make_file_name(file_descriptor, file_descriptor.message_type(i).name()),
-                false, enums, messages);
-    }
-}
-
-static void
-write_single_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor)
-{
-    int N;
-
-    vector<EnumDescriptorProto> enums;
-    N = file_descriptor.enum_type_size();
-    for (int i=0; i<N; i++) {
-        enums.push_back(file_descriptor.enum_type(i));
-    }
-
-    vector<DescriptorProto> messages;
-    N = file_descriptor.message_type_size();
-    for (int i=0; i<N; i++) {
-        messages.push_back(file_descriptor.message_type(i));
-    }
-
-    write_file(response, file_descriptor,
-            make_file_name(file_descriptor, make_outer_class_name(file_descriptor)),
-            true, enums, messages);
-}
-
-/**
  * Main.
  */
 int
@@ -273,24 +26,11 @@
 
     GOOGLE_PROTOBUF_VERIFY_VERSION;
 
-    CodeGeneratorRequest request;
-    CodeGeneratorResponse response;
-
     // Read the request
+    CodeGeneratorRequest request;
     request.ParseFromIstream(&cin);
 
-    // Build the files we need.
-    const int N = request.proto_file_size();
-    for (int i=0; i<N; i++) {
-        const FileDescriptorProto& file_descriptor = request.proto_file(i);
-        if (should_generate_for_file(request, file_descriptor.name())) {
-            if (file_descriptor.options().java_multiple_files()) {
-                write_multiple_files(&response, file_descriptor);
-            } else {
-                write_single_file(&response, file_descriptor);
-            }
-        }
-    }
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
 
     // If we had errors, don't write the response. Print the errors and exit.
     if (ERRORS.HasErrors()) {
diff --git a/tools/streaming_proto/test/imported.proto b/tools/streaming_proto/test/integration/imported.proto
similarity index 100%
rename from tools/streaming_proto/test/imported.proto
rename to tools/streaming_proto/test/integration/imported.proto
diff --git a/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java b/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java
new file mode 100644
index 0000000..2a7001b
--- /dev/null
+++ b/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.streaming_proto_test;
+
+public class Main {
+    public void main(String[] argv) {
+        System.out.println("hello world");
+    }
+}
diff --git a/tools/streaming_proto/test/test.proto b/tools/streaming_proto/test/integration/test.proto
similarity index 97%
rename from tools/streaming_proto/test/test.proto
rename to tools/streaming_proto/test/integration/test.proto
index de80ed6..3cf81b4 100644
--- a/tools/streaming_proto/test/test.proto
+++ b/tools/streaming_proto/test/integration/test.proto
@@ -16,7 +16,7 @@
 
 syntax = "proto2";
 
-import "frameworks/base/tools/streaming_proto/test/imported.proto";
+import "frameworks/base/tools/streaming_proto/test/integration/imported.proto";
 
 package com.android.streaming_proto_test;
 
diff --git a/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java b/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java
deleted file mode 100644
index 1246f53..0000000
--- a/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.android.streaming_proto_test;
-
-public class Main {
-    public void main(String[] argv) {
-        System.out.println("hello world");
-    }
-}
diff --git a/tools/streaming_proto/test/unit/streaming_proto_java.cpp b/tools/streaming_proto/test/unit/streaming_proto_java.cpp
new file mode 100644
index 0000000..8df9716
--- /dev/null
+++ b/tools/streaming_proto/test/unit/streaming_proto_java.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "java/java_proto_stream_code_generator.h"
+
+using ::testing::HasSubstr;
+using ::testing::Not;
+
+static void add_my_test_proto_file(CodeGeneratorRequest* request) {
+    request->add_file_to_generate("MyTestProtoFile");
+
+    FileDescriptorProto* file_desc = request->add_proto_file();
+    file_desc->set_name("MyTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(false);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_message");
+}
+
+static void add_my_other_test_proto_file(CodeGeneratorRequest* request) {
+    request->add_file_to_generate("MyOtherTestProtoFile");
+
+    FileDescriptorProto* file_desc = request->add_proto_file();
+    file_desc->set_name("MyOtherTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(false);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyOtherTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("a_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("another_test_field");
+}
+
+static CodeGeneratorRequest create_simple_two_file_request() {
+    CodeGeneratorRequest request;
+
+    add_my_test_proto_file(&request);
+    add_my_other_test_proto_file(&request);
+
+    return request;
+}
+
+static CodeGeneratorRequest create_simple_multi_file_request() {
+    CodeGeneratorRequest request;
+
+    request.add_file_to_generate("MyMultiMessageTestProtoFile");
+
+    FileDescriptorProto* file_desc = request.add_proto_file();
+    file_desc->set_name("MyMultiMessageTestProtoFile");
+    file_desc->set_package("test.package");
+
+    auto* file_options = file_desc->mutable_options();
+    file_options->set_java_multiple_files(true);
+
+    auto* message = file_desc->add_message_type();
+    message->set_name("MyTestMessage");
+
+    auto* field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("my_other_test_message");
+
+    message = file_desc->add_message_type();
+    message->set_name("MyOtherTestMessage");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("a_test_field");
+
+    field = message->add_field();
+    field->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
+    field->set_name("another_test_field");
+
+    return request;
+}
+
+TEST(StreamingProtoJavaTest, NoFilter) {
+    CodeGeneratorRequest request = create_simple_two_file_request();
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 2);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestProtoFile.java");
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestProtoFile"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+
+    EXPECT_EQ(response.file(1).name(), "test/package/MyOtherTestProtoFile.java");
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestProtoFile"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestMessage"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long A_TEST_FIELD"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long ANOTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithFilter) {
+    CodeGeneratorRequest request = create_simple_two_file_request();
+    request.set_parameter("include_filter:test.package.MyTestMessage");
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 1);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestProtoFile.java");
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestProtoFile"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithoutFilter_MultipleJavaFiles) {
+    CodeGeneratorRequest request = create_simple_multi_file_request();
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 2);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestMessage.java");
+    EXPECT_THAT(response.file(0).content(), Not(HasSubstr("class MyTestProtoFile")));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+
+    EXPECT_EQ(response.file(1).name(), "test/package/MyOtherTestMessage.java");
+    EXPECT_THAT(response.file(1).content(), Not(HasSubstr("class MyOtherTestProtoFile")));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestMessage"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long A_TEST_FIELD"));
+    EXPECT_THAT(response.file(1).content(), HasSubstr("long ANOTHER_TEST_FIELD"));
+}
+
+TEST(StreamingProtoJavaTest, WithFilter_MultipleJavaFiles) {
+    CodeGeneratorRequest request = create_simple_multi_file_request();
+    request.set_parameter("include_filter:test.package.MyTestMessage");
+    CodeGeneratorResponse response = generate_java_protostream_code(request);
+
+    auto generated_file_count = response.file_size();
+    EXPECT_EQ(generated_file_count, 1);
+
+    EXPECT_EQ(response.file(0).name(), "test/package/MyTestMessage.java");
+    EXPECT_THAT(response.file(0).content(), Not(HasSubstr("class MyTestProtoFile")));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD"));
+    EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD"));
+}