Merge "Remove STService dependency on model database"
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index d7163d8..5599e54 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -1933,8 +1933,9 @@
                     Intent.FLAG_RECEIVER_REGISTERED_ONLY
                             | Intent.FLAG_RECEIVER_FOREGROUND
                             | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
-            mTimeTickOptions = BroadcastOptions
-                    .makeRemovingMatchingFilter(new IntentFilter(Intent.ACTION_TIME_TICK))
+            mTimeTickOptions = BroadcastOptions.makeBasic()
+                    .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+                    .setDeferUntilActive(true)
                     .toBundle();
             mTimeTickTrigger = new IAlarmListener.Stub() {
                 @Override
@@ -4252,8 +4253,8 @@
                     }
                 }
                 // And send a TIME_TICK right now, since it is important to get the UI updated.
-                mHandler.post(() ->
-                        getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL));
+                mHandler.post(() -> getContext().sendBroadcastAsUser(mTimeTickIntent,
+                        UserHandle.ALL, null, mTimeTickOptions));
             } else {
                 mNonInteractiveStartTime = nowELAPSED;
             }
diff --git a/core/api/current.txt b/core/api/current.txt
index e27f6eb..110f818 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10853,6 +10853,7 @@
     field public static final String EXTRA_CHANGED_PACKAGE_LIST = "android.intent.extra.changed_package_list";
     field public static final String EXTRA_CHANGED_UID_LIST = "android.intent.extra.changed_uid_list";
     field public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS = "android.intent.extra.CHOOSER_CUSTOM_ACTIONS";
+    field public static final String EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION = "android.intent.extra.CHOOSER_PAYLOAD_RESELECTION_ACTION";
     field public static final String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER";
     field public static final String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS";
     field public static final String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT";
@@ -19320,7 +19321,9 @@
 
   public final class DisplayManager {
     method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int);
+    method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, float, @Nullable android.view.Surface, int);
     method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
+    method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, @IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, float, @Nullable android.view.Surface, int, @Nullable android.os.Handler, @Nullable android.hardware.display.VirtualDisplay.Callback);
     method public android.view.Display getDisplay(int);
     method public android.view.Display[] getDisplays();
     method public android.view.Display[] getDisplays(String);
@@ -24557,8 +24560,8 @@
 
   public final class RouteListingPreference implements android.os.Parcelable {
     method public int describeContents();
-    method @Nullable public android.content.ComponentName getInAppOnlyItemRoutingReceiver();
     method @NonNull public java.util.List<android.media.RouteListingPreference.Item> getItems();
+    method @Nullable public android.content.ComponentName getLinkedItemComponentName();
     method public boolean getUseSystemOrdering();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";
@@ -24569,37 +24572,39 @@
   public static final class RouteListingPreference.Builder {
     ctor public RouteListingPreference.Builder();
     method @NonNull public android.media.RouteListingPreference build();
-    method @NonNull public android.media.RouteListingPreference.Builder setInAppOnlyItemRoutingReceiver(@Nullable android.content.ComponentName);
     method @NonNull public android.media.RouteListingPreference.Builder setItems(@NonNull java.util.List<android.media.RouteListingPreference.Item>);
+    method @NonNull public android.media.RouteListingPreference.Builder setLinkedItemComponentName(@Nullable android.content.ComponentName);
     method @NonNull public android.media.RouteListingPreference.Builder setUseSystemOrdering(boolean);
   }
 
   public static final class RouteListingPreference.Item implements android.os.Parcelable {
     method public int describeContents();
-    method @Nullable public CharSequence getCustomDisableReasonMessage();
-    method public int getDisableReason();
+    method @Nullable public CharSequence getCustomSubtextMessage();
     method public int getFlags();
     method @NonNull public String getRouteId();
-    method public int getSessionParticipantCount();
+    method public int getSelectionBehavior();
+    method public int getSubText();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR;
-    field public static final int DISABLE_REASON_AD = 3; // 0x3
-    field public static final int DISABLE_REASON_CUSTOM = 5; // 0x5
-    field public static final int DISABLE_REASON_DOWNLOADED_CONTENT = 2; // 0x2
-    field public static final int DISABLE_REASON_IN_APP_ONLY = 4; // 0x4
-    field public static final int DISABLE_REASON_NONE = 0; // 0x0
-    field public static final int DISABLE_REASON_SUBSCRIPTION_REQUIRED = 1; // 0x1
     field public static final int FLAG_ONGOING_SESSION = 1; // 0x1
     field public static final int FLAG_SUGGESTED_ROUTE = 2; // 0x2
+    field public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2; // 0x2
+    field public static final int SELECTION_BEHAVIOR_NONE = 0; // 0x0
+    field public static final int SELECTION_BEHAVIOR_TRANSFER = 1; // 0x1
+    field public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 3; // 0x3
+    field public static final int SUBTEXT_CUSTOM = 10000; // 0x2710
+    field public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 2; // 0x2
+    field public static final int SUBTEXT_NONE = 0; // 0x0
+    field public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 1; // 0x1
   }
 
   public static final class RouteListingPreference.Item.Builder {
     ctor public RouteListingPreference.Item.Builder(@NonNull String);
     method @NonNull public android.media.RouteListingPreference.Item build();
-    method @NonNull public android.media.RouteListingPreference.Item.Builder setCustomDisableReasonMessage(@Nullable CharSequence);
-    method @NonNull public android.media.RouteListingPreference.Item.Builder setDisableReason(int);
+    method @NonNull public android.media.RouteListingPreference.Item.Builder setCustomSubtextMessage(@Nullable CharSequence);
     method @NonNull public android.media.RouteListingPreference.Item.Builder setFlags(int);
-    method @NonNull public android.media.RouteListingPreference.Item.Builder setSessionParticipantCount(@IntRange(from=0) int);
+    method @NonNull public android.media.RouteListingPreference.Item.Builder setSelectionBehavior(int);
+    method @NonNull public android.media.RouteListingPreference.Item.Builder setSubText(int);
   }
 
   public final class RoutingSessionInfo implements android.os.Parcelable {
@@ -51335,8 +51340,12 @@
     method public android.view.SurfaceControl getSurfaceControl();
     method public void setChildSurfacePackage(@NonNull android.view.SurfaceControlViewHost.SurfacePackage);
     method public void setSecure(boolean);
+    method public void setSurfaceLifecycle(int);
     method public void setZOrderMediaOverlay(boolean);
     method public void setZOrderOnTop(boolean);
+    field public static final int SURFACE_LIFECYCLE_DEFAULT = 0; // 0x0
+    field public static final int SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT = 2; // 0x2
+    field public static final int SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY = 1; // 0x1
   }
 
   public class TextureView extends android.view.View {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index b25e904..bd42578 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14347,7 +14347,6 @@
     field public static final String ACTION_SIM_SLOT_STATUS_CHANGED = "android.telephony.action.SIM_SLOT_STATUS_CHANGED";
     field public static final int ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G = 3; // 0x3
     field public static final int ALLOWED_NETWORK_TYPES_REASON_POWER = 1; // 0x1
-    field public static final int ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS = 4; // 0x4
     field public static final int CALL_WAITING_STATUS_DISABLED = 2; // 0x2
     field public static final int CALL_WAITING_STATUS_ENABLED = 1; // 0x1
     field public static final int CALL_WAITING_STATUS_FDN_CHECK_FAILURE = 5; // 0x5
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 5f2f623..7a460ee 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -464,8 +464,11 @@
 
   public class WallpaperManager {
     method @Nullable public android.graphics.Bitmap getBitmap();
+    method @Nullable public android.graphics.Bitmap getBitmapAsUser(int, boolean, int);
+    method public boolean isLockscreenLiveWallpaperEnabled();
     method @Nullable public android.graphics.Rect peekBitmapDimensions();
     method @Nullable public android.graphics.Rect peekBitmapDimensions(int);
+    method public void setWallpaperZoomOut(@NonNull android.os.IBinder, float);
     method public boolean shouldEnableWideColorGamut();
     method @RequiresPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) public boolean wallpaperSupportsWcg(int);
   }
@@ -1869,7 +1872,10 @@
   public class Build {
     method public static boolean is64BitAbi(String);
     method public static boolean isDebuggable();
+    field @Nullable public static final String BRAND_FOR_ATTESTATION;
     field public static final boolean IS_EMULATOR;
+    field @Nullable public static final String MODEL_FOR_ATTESTATION;
+    field @Nullable public static final String PRODUCT_FOR_ATTESTATION;
   }
 
   public static class Build.VERSION {
@@ -2067,6 +2073,7 @@
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getUsers(boolean, boolean, boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean hasBaseUserRestriction(@NonNull String, @NonNull android.os.UserHandle);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isUserTypeEnabled(@NonNull String);
+    method public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported();
     method public boolean isVisibleBackgroundUsersSupported();
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException;
   }
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index f9e4081..73f34eb 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -42,7 +42,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.database.DatabaseUtils;
-import android.healthconnect.HealthConnectManager;
+import android.health.connect.HealthConnectManager;
 import android.media.AudioAttributes.AttributeUsage;
 import android.os.Binder;
 import android.os.Build;
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 52d1d68..031d351 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -116,7 +116,7 @@
 import android.hardware.radio.RadioManager;
 import android.hardware.usb.IUsbManager;
 import android.hardware.usb.UsbManager;
-import android.healthconnect.HealthServicesInitializer;
+import android.health.connect.HealthServicesInitializer;
 import android.location.CountryDetector;
 import android.location.ICountryDetector;
 import android.location.ILocationManager;
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index f3a83d8..a2cacc5 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -784,6 +784,7 @@
      * @return true if the lockscreen wallpaper always uses a wallpaperService, not a static image
      * @hide
      */
+    @TestApi
     public boolean isLockscreenLiveWallpaperEnabled() {
         return mLockscreenLiveWallpaper;
     }
@@ -1258,6 +1259,8 @@
      * @param which Specifies home or lock screen
      * @hide
      */
+    @TestApi
+    @Nullable
     public Bitmap getBitmapAsUser(int userId, boolean hardware, @SetWallpaperFlags int which) {
         final ColorManagementProxy cmProxy = getColorManagementProxy();
         return sGlobals.peekWallpaperBitmap(mContext, true, which, userId, hardware, cmProxy);
@@ -2421,6 +2424,7 @@
      *
      * @hide
      */
+    @TestApi
     public void setWallpaperZoomOut(@NonNull IBinder windowToken, float zoom) {
         if (zoom < 0 || zoom > 1f) {
             throw new IllegalArgumentException("zoom must be between 0 and 1: " + zoom);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 5f1502f..387e8ae 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -6151,10 +6151,10 @@
 
     /**
      * Use with {@link #getSystemService(String)} to retrieve a
-     * {@link android.healthconnect.HealthConnectManager}.
+     * {@link android.health.connect.HealthConnectManager}.
      *
      * @see #getSystemService(String)
-     * @see android.healthconnect.HealthConnectManager
+     * @see android.health.connect.HealthConnectManager
      */
     public static final String HEALTHCONNECT_SERVICE = "healthconnect";
 
@@ -6198,6 +6198,16 @@
     public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection";
 
     /**
+     * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.telephony.satellite.SatelliteManager} for accessing satellite functionality.
+     *
+     * @see #getSystemService(String)
+     * @see android.telephony.satellite.SatelliteManager
+     * @hide
+     */
+    public static final String SATELLITE_SERVICE = "satellite";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 5714032..30984b2 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5829,7 +5829,6 @@
      * in the sharesheet.
      * A reselection action allows the user to return to the source app to change the content being
      * shared.
-     * @hide
      */
     public static final String EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION =
             "android.intent.extra.CHOOSER_PAYLOAD_RESELECTION_ACTION";
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 08238ca..d13a97d 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -66,6 +66,7 @@
 public final class DisplayManager {
     private static final String TAG = "DisplayManager";
     private static final boolean DEBUG = false;
+    private static final boolean ENABLE_VIRTUAL_DISPLAY_REFRESH_RATE = false;
 
     private final Context mContext;
     private final DisplayManagerGlobal mGlobal;
@@ -938,6 +939,24 @@
 
     /**
      * Creates a virtual display.
+     *
+     * @see #createVirtualDisplay(String, int, int, int, float, Surface, int,
+     * Handler, VirtualDisplay.Callback)
+     */
+    @Nullable
+    public VirtualDisplay createVirtualDisplay(@NonNull String name,
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @IntRange(from = 1) int densityDpi,
+            float requestedRefreshRate,
+            @Nullable Surface surface,
+            @VirtualDisplayFlag int flags) {
+        return createVirtualDisplay(name, width, height, densityDpi, requestedRefreshRate,
+                surface, flags, null, null);
+    }
+
+    /**
+     * Creates a virtual display.
      * <p>
      * The content of a virtual display is rendered to a {@link Surface} provided
      * by the application.
@@ -987,12 +1006,82 @@
             @VirtualDisplayFlag int flags,
             @Nullable VirtualDisplay.Callback callback,
             @Nullable Handler handler) {
+        return createVirtualDisplay(name, width, height, densityDpi, 0.0f, surface,
+                flags, handler, callback);
+    }
+
+    /**
+     * Creates a virtual display.
+     * <p>
+     * The content of a virtual display is rendered to a {@link Surface} provided
+     * by the application.
+     * </p><p>
+     * The virtual display should be {@link VirtualDisplay#release released}
+     * when no longer needed.  Because a virtual display renders to a surface
+     * provided by the application, it will be released automatically when the
+     * process terminates and all remaining windows on it will be forcibly removed.
+     * </p><p>
+     * The behavior of the virtual display depends on the flags that are provided
+     * to this method.  By default, virtual displays are created to be private,
+     * non-presentation and unsecure.  Permissions may be required to use certain flags.
+     * </p><p>
+     * As of {@link android.os.Build.VERSION_CODES#KITKAT_WATCH}, the surface may
+     * be attached or detached dynamically using {@link VirtualDisplay#setSurface}.
+     * Previously, the surface had to be non-null when {@link #createVirtualDisplay}
+     * was called and could not be changed for the lifetime of the display.
+     * </p><p>
+     * Detaching the surface that backs a virtual display has a similar effect to
+     * turning off the screen.
+     * </p>
+     *
+     * @param name The name of the virtual display, must be non-empty.
+     * @param width The width of the virtual display in pixels, must be greater than 0.
+     * @param height The height of the virtual display in pixels, must be greater than 0.
+     * @param densityDpi The density of the virtual display in dpi, must be greater than 0.
+     * @param requestedRefreshRate The requested refresh rate in frames per second.
+     * For best results, specify a divisor of the physical refresh rate, e.g., 30 or 60 on
+     * 120hz display. If an arbitrary refresh rate is specified, the rate will be rounded
+     * up or down to a divisor of the physical display. If 0 is specified, the virtual
+     * display is refreshed at the physical display refresh rate.
+     * @param surface The surface to which the content of the virtual display should
+     * be rendered, or null if there is none initially.
+     * @param flags A combination of virtual display flags:
+     * {@link #VIRTUAL_DISPLAY_FLAG_PUBLIC}, {@link #VIRTUAL_DISPLAY_FLAG_PRESENTATION},
+     * {@link #VIRTUAL_DISPLAY_FLAG_SECURE}, {@link #VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY},
+     * or {@link #VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR}.
+     * @param handler The handler on which the listener should be invoked, or null
+     * if the listener should be invoked on the calling thread's looper.
+     * @param callback Callback to call when the state of the {@link VirtualDisplay} changes
+     * @return The newly created virtual display, or null if the application could
+     * not create the virtual display.
+     *
+     * @throws SecurityException if the caller does not have permission to create
+     * a virtual display with the specified flags.
+     */
+    @Nullable
+    public VirtualDisplay createVirtualDisplay(@NonNull String name,
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @IntRange(from = 1) int densityDpi,
+            float requestedRefreshRate,
+            @Nullable Surface surface,
+            @VirtualDisplayFlag int flags,
+            @Nullable Handler handler,
+            @Nullable VirtualDisplay.Callback callback) {
+        if (!ENABLE_VIRTUAL_DISPLAY_REFRESH_RATE && requestedRefreshRate != 0.0f) {
+            Slog.e(TAG, "Please turn on ENABLE_VIRTUAL_DISPLAY_REFRESH_RATE to use the new api");
+            return null;
+        }
+
         final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width,
                 height, densityDpi);
         builder.setFlags(flags);
         if (surface != null) {
             builder.setSurface(surface);
         }
+        if (requestedRefreshRate != 0.0f) {
+            builder.setRequestedRefreshRate(requestedRefreshRate);
+        }
         return createVirtualDisplay(null /* projection */, builder.build(), callback, handler,
                 null /* windowContext */);
     }
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index abd647f..f6a2e33 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -109,6 +109,13 @@
     @DataClass.PluralOf("displayCategory")
     @NonNull private List<String> mDisplayCategories = new ArrayList<>();
 
+    /**
+     * The refresh rate of a virtual display in frames per second.
+     * If this value is non-zero, this is the requested refresh rate to set.
+     * If this value is zero, the system chooses a default refresh rate.
+     */
+    private float mRequestedRefreshRate = 0.0f;
+
 
 
     // Code below generated by codegen v1.0.23.
@@ -135,7 +142,8 @@
             @Nullable String uniqueId,
             int displayIdToMirror,
             boolean windowManagerMirroring,
-            @NonNull List<String> displayCategories) {
+            @NonNull List<String> displayCategories,
+            float requestedRefreshRate) {
         this.mName = name;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mName);
@@ -161,6 +169,7 @@
         this.mDisplayCategories = displayCategories;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mDisplayCategories);
+        this.mRequestedRefreshRate = requestedRefreshRate;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -256,6 +265,16 @@
         return mDisplayCategories;
     }
 
+    /**
+     * The refresh rate of a virtual display in frames per second.
+     * If this value is none zero, this is the requested refresh rate to set.
+     * If this value is zero, the system chooses a default refresh rate.
+     */
+    @DataClass.Generated.Member
+    public float getRequestedRefreshRate() {
+        return mRequestedRefreshRate;
+    }
+
     @Override
     @DataClass.Generated.Member
     public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -276,6 +295,7 @@
         if (mUniqueId != null) dest.writeString(mUniqueId);
         dest.writeInt(mDisplayIdToMirror);
         dest.writeStringList(mDisplayCategories);
+        dest.writeFloat(mRequestedRefreshRate);
     }
 
     @Override
@@ -301,6 +321,7 @@
         int displayIdToMirror = in.readInt();
         List<String> displayCategories = new ArrayList<>();
         in.readStringList(displayCategories);
+        float requestedRefreshRate = in.readFloat();
 
         this.mName = name;
         com.android.internal.util.AnnotationValidations.validate(
@@ -327,6 +348,7 @@
         this.mDisplayCategories = displayCategories;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mDisplayCategories);
+        this.mRequestedRefreshRate = requestedRefreshRate;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -362,6 +384,7 @@
         private int mDisplayIdToMirror;
         private boolean mWindowManagerMirroring;
         private @NonNull List<String> mDisplayCategories;
+        private float mRequestedRefreshRate;
 
         private long mBuilderFieldsSet = 0L;
 
@@ -528,10 +551,23 @@
             return this;
         }
 
+        /**
+         * The refresh rate of a virtual display in frames per second.
+         * If this value is none zero, this is the requested refresh rate to set.
+         * If this value is zero, the system chooses a default refresh rate.
+         */
+        @DataClass.Generated.Member
+        public @NonNull Builder setRequestedRefreshRate(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x400;
+            mRequestedRefreshRate = value;
+            return this;
+        }
+
         /** Builds the instance. This builder should not be touched after calling this! */
         public @NonNull VirtualDisplayConfig build() {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x400; // Mark builder used
+            mBuilderFieldsSet |= 0x800; // Mark builder used
 
             if ((mBuilderFieldsSet & 0x10) == 0) {
                 mFlags = 0;
@@ -551,6 +587,9 @@
             if ((mBuilderFieldsSet & 0x200) == 0) {
                 mDisplayCategories = new ArrayList<>();
             }
+            if ((mBuilderFieldsSet & 0x400) == 0) {
+                mRequestedRefreshRate = 0.0f;
+            }
             VirtualDisplayConfig o = new VirtualDisplayConfig(
                     mName,
                     mWidth,
@@ -561,12 +600,13 @@
                     mUniqueId,
                     mDisplayIdToMirror,
                     mWindowManagerMirroring,
-                    mDisplayCategories);
+                    mDisplayCategories,
+                    mRequestedRefreshRate);
             return o;
         }
 
         private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x400) != 0) {
+            if ((mBuilderFieldsSet & 0x800) != 0) {
                 throw new IllegalStateException(
                         "This Builder should not be reused. Use a new Builder instance instead");
             }
@@ -574,10 +614,10 @@
     }
 
     @DataClass.Generated(
-            time = 1668534501320L,
+            time = 1671047069703L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/hardware/display/VirtualDisplayConfig.java",
-            inputSignatures = "private @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.IntRange int mWidth\nprivate @android.annotation.IntRange int mHeight\nprivate @android.annotation.IntRange int mDensityDpi\nprivate @android.hardware.display.DisplayManager.VirtualDisplayFlag int mFlags\nprivate @android.annotation.Nullable android.view.Surface mSurface\nprivate @android.annotation.Nullable java.lang.String mUniqueId\nprivate  int mDisplayIdToMirror\nprivate  boolean mWindowManagerMirroring\nprivate @com.android.internal.util.DataClass.PluralOf(\"displayCategory\") @android.annotation.NonNull java.util.List<java.lang.String> mDisplayCategories\nclass VirtualDisplayConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true)")
+            inputSignatures = "private @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.IntRange int mWidth\nprivate @android.annotation.IntRange int mHeight\nprivate @android.annotation.IntRange int mDensityDpi\nprivate @android.hardware.display.DisplayManager.VirtualDisplayFlag int mFlags\nprivate @android.annotation.Nullable android.view.Surface mSurface\nprivate @android.annotation.Nullable java.lang.String mUniqueId\nprivate  int mDisplayIdToMirror\nprivate  boolean mWindowManagerMirroring\nprivate @com.android.internal.util.DataClass.PluralOf(\"displayCategory\") @android.annotation.NonNull java.util.List<java.lang.String> mDisplayCategories\nprivate  float mRequestedRefreshRate\nclass VirtualDisplayConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 249f486..832f23c 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -61,6 +61,17 @@
     /** The name of the overall product. */
     public static final String PRODUCT = getString("ro.product.name");
 
+    /**
+     * The product name for attestation. In non-default builds (like the AOSP build) the value of
+     * the 'PRODUCT' system property may be different to the one provisioned to KeyMint,
+     * and Keymint attestation would still attest to the product name, it's running on.
+     * @hide
+     */
+    @Nullable
+    @TestApi
+    public static final String PRODUCT_FOR_ATTESTATION =
+            getString("ro.product.name_for_attestation");
+
     /** The name of the industrial design. */
     public static final String DEVICE = getString("ro.product.device");
 
@@ -89,9 +100,31 @@
     /** The consumer-visible brand with which the product/hardware will be associated, if any. */
     public static final String BRAND = getString("ro.product.brand");
 
+    /**
+     * The product brand for attestation. In non-default builds (like the AOSP build) the value of
+     * the 'BRAND' system property may be different to the one provisioned to KeyMint,
+     * and Keymint attestation would still attest to the product brand, it's running on.
+     * @hide
+     */
+    @Nullable
+    @TestApi
+    public static final String BRAND_FOR_ATTESTATION =
+                getString("ro.product.brand_for_attestation");
+
     /** The end-user-visible name for the end product. */
     public static final String MODEL = getString("ro.product.model");
 
+    /**
+     * The product model for attestation. In non-default builds (like the AOSP build) the value of
+     * the 'MODEL' system property may be different to the one provisioned to KeyMint,
+     * and Keymint attestation would still attest to the product model, it's running on.
+     * @hide
+     */
+    @Nullable
+    @TestApi
+    public static final String MODEL_FOR_ATTESTATION =
+                getString("ro.product.model_for_attestation");
+
     /** The manufacturer of the device's primary system-on-chip. */
     @NonNull
     public static final String SOC_MANUFACTURER = SocProperties.soc_manufacturer().orElse(UNKNOWN);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index d63d87d..0efd264 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2976,6 +2976,29 @@
     }
 
     /**
+     * @hide
+     */
+    public static boolean isVisibleBackgroundUsersOnDefaultDisplayEnabled() {
+        return SystemProperties.getBoolean("fw.visible_bg_users_on_default_display",
+                Resources.getSystem()
+                        .getBoolean(R.bool.config_multiuserVisibleBackgroundUsersOnDefaultDisplay));
+    }
+
+    /**
+     * Returns whether the device allows (full) users to be started in background visible in the
+     * {@link android.view.Display#DEFAULT_DISPLAY default display}.
+     *
+     * @return {@code false} for most devices, except passenger-only automotive build (i.e., when
+     * Android runs in a separate system in the back seat to manage the passenger displays).
+     *
+     * @hide
+     */
+    @TestApi
+    public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported() {
+        return isVisibleBackgroundUsersOnDefaultDisplayEnabled();
+    }
+
+    /**
      * Checks if the user is visible at the moment.
      *
      * <p>Roughly speaking, a "visible user" is a user that can present UI on at least one display.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b2d89962..4f96805 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7392,6 +7392,9 @@
          *
          * Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0"
          * where imeId is ComponentName and subtype is int32.
+         *
+         * <p>Note: This setting is not readable to the app targeting API level 34 or higher. use
+         * {@link android.view.inputmethod.InputMethodManager#getEnabledInputMethodList()} instead.
          */
         @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU)
         public static final String ENABLED_INPUT_METHODS = "enabled_input_methods";
@@ -15498,6 +15501,25 @@
         public static final String AUDIO_SAFE_VOLUME_STATE = "audio_safe_volume_state";
 
         /**
+         * Persisted safe hearding current CSD value. Values are stored as float percentages where
+         * 1.f represents 100% sound dose has been reached.
+         * @hide
+         */
+        public static final String AUDIO_SAFE_CSD_CURRENT_VALUE = "audio_safe_csd_current_value";
+
+        /**
+         * Persisted safe hearding next CSD warning value. Values are stored as float percentages.
+         * @hide
+         */
+        public static final String AUDIO_SAFE_CSD_NEXT_WARNING = "audio_safe_csd_next_warning";
+
+        /**
+         * Persisted safe hearding dose records (see {@link android.media.SoundDoseRecord})
+         * @hide
+         */
+        public static final String AUDIO_SAFE_CSD_DOSE_RECORDS = "audio_safe_csd_dose_records";
+
+        /**
          * URL for tzinfo (time zone) updates
          * @hide
          */
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 1eb87c9..0bce710 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -20,6 +20,7 @@
 import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER;
 import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -51,6 +52,8 @@
 
 import com.android.internal.view.SurfaceCallbackHelper;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.locks.ReentrantLock;
@@ -117,6 +120,36 @@
  * may not blend properly as a consequence of not applying alpha to the surface content directly.
  */
 public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCallback {
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"SURFACE_LIFECYCLE_"},
+            value = {SURFACE_LIFECYCLE_DEFAULT,
+                     SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY,
+                     SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT})
+    public @interface SurfaceLifecycleStrategy {}
+
+    /**
+     * Default lifecycle of the Surface owned by this SurfaceView.
+     *
+     * The default lifecycle matches {@link #SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY}.
+     */
+    public static final int SURFACE_LIFECYCLE_DEFAULT = 0;
+
+    /**
+     * The Surface lifecycle is tied to SurfaceView visibility.
+     *
+     * The Surface is created when the SurfaceView becomes visible, and is destroyed when the
+     * SurfaceView is no longer visible.
+     */
+    public static final int SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY = 1;
+
+    /**
+     * The Surface lifecycle is tied to SurfaceView attachment.
+     * The Surface is created when the SurfaceView first becomes attached, but is not destroyed
+     * until this SurfaceView has been detached from the current window.
+     */
+    public static final int SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT = 2;
+
     private static final String TAG = "SurfaceView";
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_POSITION = false;
@@ -151,6 +184,11 @@
     SurfaceControl mBackgroundControl;
     private boolean mDisableBackgroundLayer = false;
 
+    @SurfaceLifecycleStrategy
+    private int mRequestedSurfaceLifecycleStrategy = SURFACE_LIFECYCLE_DEFAULT;
+    @SurfaceLifecycleStrategy
+    private int mSurfaceLifecycleStrategy = SURFACE_LIFECYCLE_DEFAULT;
+
     /**
      * We use this lock to protect access to mSurfaceControl. Both are accessed on the UI
      * thread and the render thread via RenderNode.PositionUpdateListener#positionLost.
@@ -413,6 +451,7 @@
             observer.removeOnPreDrawListener(mDrawListener);
             mGlobalListenersAdded = false;
         }
+        if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " Detaching SV");
 
         mRequestedVisible = false;
 
@@ -699,6 +738,20 @@
         }
     }
 
+    /**
+     * Controls the lifecycle of the Surface owned by this SurfaceView.
+     *
+     * <p>By default, the lifecycycle strategy employed by the SurfaceView is
+     * {@link #SURFACE_LIFECYCLE_DEFAULT}.
+     *
+     * @param lifecycleStrategy The strategy for the lifecycle of the Surface owned by this
+     * SurfaceView.
+     */
+    public void setSurfaceLifecycle(@SurfaceLifecycleStrategy int lifecycleStrategy) {
+        mRequestedSurfaceLifecycleStrategy = lifecycleStrategy;
+        updateSurface();
+    }
+
     private void updateOpaqueFlag() {
         if (!PixelFormat.formatHasAlpha(mRequestedFormat)) {
             mSurfaceFlags |= SurfaceControl.OPAQUE;
@@ -780,7 +833,7 @@
 
         mSurfaceLock.lock();
         try {
-            mDrawingStopped = !mVisible;
+            mDrawingStopped = !surfaceShouldExist();
 
             if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
                     + "Cur surface: " + mSurface);
@@ -887,6 +940,20 @@
         return realSizeChanged;
     }
 
+    private boolean requiresSurfaceControlCreation(boolean formatChanged, boolean visibleChanged) {
+        if (mSurfaceLifecycleStrategy == SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT) {
+            return (mSurfaceControl == null || formatChanged) && mAttachedToWindow;
+        }
+
+        return (mSurfaceControl == null || formatChanged || visibleChanged) && mRequestedVisible;
+    }
+
+    private boolean surfaceShouldExist() {
+        final boolean respectVisibility =
+                mSurfaceLifecycleStrategy != SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT;
+        return mVisible || (!respectVisibility && mAttachedToWindow);
+    }
+
     /** @hide */
     protected void updateSurface() {
         if (!mHaveFrame) {
@@ -921,8 +988,7 @@
         final boolean formatChanged = mFormat != mRequestedFormat;
         final boolean visibleChanged = mVisible != mRequestedVisible;
         final boolean alphaChanged = mAlpha != alpha;
-        final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged)
-                && mRequestedVisible;
+        final boolean creating = requiresSurfaceControlCreation(formatChanged, visibleChanged);
         final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight;
         final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility;
         getLocationInWindow(mLocation);
@@ -933,10 +999,13 @@
         final boolean hintChanged = (viewRoot.getBufferTransformHint() != mTransformHint)
                 && mRequestedVisible;
         final boolean relativeZChanged = mSubLayer != mRequestedSubLayer;
+        final boolean surfaceLifecycleStrategyChanged =
+                mSurfaceLifecycleStrategy != mRequestedSurfaceLifecycleStrategy;
 
         if (creating || formatChanged || sizeChanged || visibleChanged
                 || alphaChanged || windowVisibleChanged || positionChanged
-                || layoutSizeChanged || hintChanged || relativeZChanged) {
+                || layoutSizeChanged || hintChanged || relativeZChanged || !mAttachedToWindow
+                || surfaceLifecycleStrategyChanged) {
 
             if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
                     + "Changes: creating=" + creating
@@ -945,7 +1014,10 @@
                     + " hint=" + hintChanged
                     + " visible=" + visibleChanged
                     + " left=" + (mWindowSpaceLeft != mLocation[0])
-                    + " top=" + (mWindowSpaceTop != mLocation[1]));
+                    + " top=" + (mWindowSpaceTop != mLocation[1])
+                    + " z=" + relativeZChanged
+                    + " attached=" + mAttachedToWindow
+                    + " lifecycleStrategy=" + surfaceLifecycleStrategyChanged);
 
             try {
                 mVisible = mRequestedVisible;
@@ -959,6 +1031,9 @@
                 mTransformHint = viewRoot.getBufferTransformHint();
                 mSubLayer = mRequestedSubLayer;
 
+                final int previousSurfaceLifecycleStrategy = mSurfaceLifecycleStrategy;
+                mSurfaceLifecycleStrategy = mRequestedSurfaceLifecycleStrategy;
+
                 mScreenRect.left = mWindowSpaceLeft;
                 mScreenRect.top = mWindowSpaceTop;
                 mScreenRect.right = mWindowSpaceLeft + getWidth();
@@ -1000,15 +1075,27 @@
                     SurfaceHolder.Callback[] callbacks = null;
 
                     final boolean surfaceChanged = creating;
-                    if (mSurfaceCreated && (surfaceChanged || (!mVisible && visibleChanged))) {
-                        mSurfaceCreated = false;
-                        notifySurfaceDestroyed();
+                    final boolean respectVisibility =
+                            mSurfaceLifecycleStrategy != SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT;
+                    final boolean previouslyDidNotRespectVisibility =
+                            previousSurfaceLifecycleStrategy
+                                    == SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT;
+                    final boolean lifecycleNewlyRespectsVisibility = respectVisibility
+                            && previouslyDidNotRespectVisibility;
+                    if (mSurfaceCreated) {
+                        if (surfaceChanged || (!respectVisibility && !mAttachedToWindow)
+                                || (respectVisibility && !mVisible
+                                        && (visibleChanged || lifecycleNewlyRespectsVisibility))) {
+                            mSurfaceCreated = false;
+                            notifySurfaceDestroyed();
+                        }
                     }
 
                     copySurface(creating /* surfaceControlCreated */, sizeChanged);
 
-                    if (mVisible && mSurface.isValid()) {
-                        if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) {
+                    if (surfaceShouldExist() && mSurface.isValid()) {
+                        if (!mSurfaceCreated
+                                && (surfaceChanged || (respectVisibility && visibleChanged))) {
                             mSurfaceCreated = true;
                             mIsCreating = true;
                             if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
@@ -1019,7 +1106,7 @@
                             }
                         }
                         if (creating || formatChanged || sizeChanged || hintChanged
-                                || visibleChanged || realSizeChanged) {
+                                || (respectVisibility && visibleChanged) || realSizeChanged) {
                             if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
                                     + "surfaceChanged -- format=" + mFormat
                                     + " w=" + myWidth + " h=" + myHeight);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 089545a..be58893 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -108,6 +108,7 @@
 import android.text.InputFilter;
 import android.text.InputType;
 import android.text.Layout;
+import android.text.NoCopySpan;
 import android.text.ParcelableSpan;
 import android.text.PrecomputedText;
 import android.text.SegmentFinder;
@@ -905,6 +906,8 @@
     private Scroller mScroller;
     private TextPaint mTempTextPaint;
 
+    private Object mTempCursor;
+
     @UnsupportedAppUsage
     private BoringLayout.Metrics mBoring;
     @UnsupportedAppUsage
@@ -10060,10 +10063,8 @@
         }
         int offset = mLayout.getOffsetForHorizontal(line, point.x);
         String textToInsert = gesture.getTextToInsert();
-        getEditableText().insert(offset, textToInsert);
-        Selection.setSelection(getEditableText(), offset + textToInsert.length());
+        return tryInsertTextForHandwritingGesture(offset, textToInsert, gesture);
         // TODO(b/243980426): Insert extra spaces if necessary.
-        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
@@ -10163,12 +10164,11 @@
         if (startOffset < endOffset) {
             getEditableText().delete(startOffset, endOffset);
             Selection.setSelection(getEditableText(), startOffset);
+            return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
         } else {
             // No whitespace found, so insert a space.
-            getEditableText().insert(startOffset, " ");
-            Selection.setSelection(getEditableText(), startOffset + 1);
+            return tryInsertTextForHandwritingGesture(startOffset, " ", gesture);
         }
-        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
@@ -10252,6 +10252,32 @@
                 area, segmentFinder, Layout.INCLUSION_STRATEGY_CONTAINS_CENTER);
     }
 
+    private int tryInsertTextForHandwritingGesture(
+            int offset, String textToInsert, HandwritingGesture gesture) {
+        // A temporary cursor span is placed at the insertion offset. The span will be pushed
+        // forward when text is inserted, then the real cursor can be placed after the inserted
+        // text. A temporary cursor span is used in order to avoid modifying the real selection span
+        // in the case that the text is filtered out.
+        Editable editableText = getEditableText();
+        if (mTempCursor == null) {
+            mTempCursor = new NoCopySpan.Concrete();
+        }
+        editableText.setSpan(mTempCursor, offset, offset, Spanned.SPAN_POINT_POINT);
+
+        editableText.insert(offset, textToInsert);
+
+        int newOffset = editableText.getSpanStart(mTempCursor);
+        editableText.removeSpan(mTempCursor);
+        if (newOffset == offset) {
+            // The inserted text was filtered out.
+            return handleGestureFailure(gesture);
+        } else {
+            // Place the cursor after the inserted text.
+            Selection.setSelection(editableText, newOffset);
+            return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+        }
+    }
+
     private Pattern getWhitespacePattern() {
         if (mWhitespacePattern == null) {
             mWhitespacePattern = Pattern.compile("\\s+");
diff --git a/core/java/com/android/internal/app/OWNERS b/core/java/com/android/internal/app/OWNERS
index 56da8e0..69660ae 100644
--- a/core/java/com/android/internal/app/OWNERS
+++ b/core/java/com/android/internal/app/OWNERS
@@ -11,6 +11,7 @@
 per-file *Assist* = file:/core/java/android/service/voice/OWNERS
 per-file *Hotword* = file:/core/java/android/service/voice/OWNERS
 per-file *Voice* = file:/core/java/android/service/voice/OWNERS
+per-file *VisualQuery* = file:/core/java/android/service/voice/OWNERS
 
 # System language settings
 per-file *Locale* = file:platform/packages/apps/Settings:/src/com/android/settings/localepicker/OWNERS
diff --git a/core/proto/android/providers/settings/global.proto b/core/proto/android/providers/settings/global.proto
index fd0d9c5..128de8b 100644
--- a/core/proto/android/providers/settings/global.proto
+++ b/core/proto/android/providers/settings/global.proto
@@ -88,6 +88,9 @@
 
     optional SettingProto assisted_gps_enabled = 14 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto audio_safe_volume_state = 15 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    optional SettingProto audio_safe_csd_current_value = 157 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    optional SettingProto audio_safe_csd_next_warning = 158 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    optional SettingProto audio_safe_csd_dose_records = 159 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     reserved 17; // Used to be autofill_compat_mode_allowed_packages
 
@@ -1087,5 +1090,5 @@
 
     // Please insert fields in alphabetical order and group them into messages
     // if possible (to avoid reaching the method limit).
-    // Next tag = 157;
+    // Next tag = 160;
 }
diff --git a/core/res/res/drawable/ic_clone_badge.xml b/core/res/res/drawable/ic_clone_badge.xml
new file mode 100644
index 0000000..1682425
--- /dev/null
+++ b/core/res/res/drawable/ic_clone_badge.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:pathData="M22,9.5C22,13.642 18.642,17 14.5,17C10.358,17 7,13.642 7,9.5C7,5.358 10.358,2 14.5,2C18.642,2 22,5.358 22,9.5Z"
+        android:fillColor="@android:color/system_neutral2_800"/>
+    <path
+        android:pathData="M9.5,20.333C12.722,20.333 15.333,17.722 15.333,14.5C15.333,11.278 12.722,8.667 9.5,8.667C6.278,8.667 3.667,11.278 3.667,14.5C3.667,17.722 6.278,20.333 9.5,20.333ZM9.5,22C13.642,22 17,18.642 17,14.5C17,10.358 13.642,7 9.5,7C5.358,7 2,10.358 2,14.5C2,18.642 5.358,22 9.5,22Z"
+        android:fillColor="@android:color/system_neutral2_800"
+        android:fillType="evenOdd"/>
+</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_clone_icon_badge.xml b/core/res/res/drawable/ic_clone_icon_badge.xml
new file mode 100644
index 0000000..e4eabc8
--- /dev/null
+++ b/core/res/res/drawable/ic_clone_icon_badge.xml
@@ -0,0 +1,35 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="64dp"
+        android:height="64dp"
+        android:viewportWidth="64"
+        android:viewportHeight="64">
+<group
+    android:scaleX=".66"
+    android:scaleY=".66"
+    android:translateX="42"
+    android:translateY="42">
+    <path
+        android:pathData="M22,9.5C22,13.642 18.642,17 14.5,17C10.358,17 7,13.642 7,9.5C7,5.358 10.358,2 14.5,2C18.642,2 22,5.358 22,9.5Z"
+        android:fillColor="@android:color/system_neutral2_800"/>
+    <path
+        android:pathData="M9.5,20.333C12.722,20.333 15.333,17.722 15.333,14.5C15.333,11.278 12.722,8.667 9.5,8.667C6.278,8.667 3.667,11.278 3.667,14.5C3.667,17.722 6.278,20.333 9.5,20.333ZM9.5,22C13.642,22 17,18.642 17,14.5C17,10.358 13.642,7 9.5,7C5.358,7 2,10.358 2,14.5C2,18.642 5.358,22 9.5,22Z"
+        android:fillColor="@android:color/system_neutral2_800"
+        android:fillType="evenOdd"/>
+</group>
+</vector>
diff --git a/core/res/res/values/arrays.xml b/core/res/res/values/arrays.xml
index 9e735d0..c3366cf 100644
--- a/core/res/res/values/arrays.xml
+++ b/core/res/res/values/arrays.xml
@@ -207,4 +207,25 @@
          be set by OEMs for devices which use eUICCs. -->
     <integer-array name="non_removable_euicc_slots"></integer-array>
 
+    <!-- Device state identifiers and strings for system notifications. The string arrays must have
+         the same length and order as the identifier array. -->
+    <integer-array name="device_state_notification_state_identifiers">
+        <!-- TODO(b/267231269) change to concurrent display identifier when ready -->
+        <item>-1</item>
+    </integer-array>
+    <string-array name="device_state_notification_names">
+        <item>@string/concurrent_display_notification_name</item>
+    </string-array>
+    <string-array name="device_state_notification_active_titles">
+        <item>@string/concurrent_display_notification_active_title</item>
+    </string-array>
+    <string-array name="device_state_notification_active_contents">
+        <item>@string/concurrent_display_notification_active_content</item>
+    </string-array>
+    <string-array name="device_state_notification_thermal_titles">
+        <item>@string/concurrent_display_notification_thermal_title</item>
+    </string-array>
+    <string-array name="device_state_notification_thermal_contents">
+        <item>@string/concurrent_display_notification_thermal_content</item>
+    </string-array>
 </resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b8ce7e3..37d5b84 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2735,6 +2735,11 @@
          Should be false for most devices, except automotive vehicle with passenger displays. -->
     <bool name="config_multiuserVisibleBackgroundUsers">false</bool>
 
+    <!-- Whether the device allows users to start in background visible on the default display.
+         Should be false for most devices, except passenger-only automotive build (i.e., when
+         Android runs in a separate system in the back seat to manage the passenger displays) -->
+    <bool name="config_multiuserVisibleBackgroundUsersOnDefaultDisplay">false</bool>
+
     <!-- Whether to automatically switch to the designated Dock User (the user chosen for
          displaying dreams, etc.) after a timeout when the device is docked.  -->
     <bool name="config_enableTimeoutToDockUserWhenDocked">false</bool>
@@ -5001,6 +5006,26 @@
       -->
     </integer-array>
 
+    <!-- The properties for handling UDFPS touch detection
+    <string-array name="config_udfps_touch_detection_options">
+        <item>[detector_type],[sensor_shape],[target_size],[min_ellipse_overlap_percentage]</item>
+    </string-array>
+
+        [detector_type]: 0 for bounding box detector, 1 for ellipse detector
+        [sensor_shape]: 0 for square, 1 for circle
+        [target_size]: percentage (defined as a float of 0-1) of sensor that is considered the target, expands outward from center
+        [min_ellipse_overlap_percentage]: minimum percentage (float from 0-1) needed to be considered a valid overlap
+        [step_size]: size of each step when iterating over sensor pixel grid
+    -->
+    <string-array name="config_udfps_touch_detection_options">
+        <item>0,0,1.0,0,1</item>
+        <item>1,1,1.0,0,1</item>
+        <item>1,1,1.0,.4,1</item>
+    </string-array>
+
+    <!-- The integer index of the selected option in config_udfps_touch_detection_options -->
+    <integer name="config_selected_udfps_touch_detection">2</integer>
+
     <!-- An array of arrays of side fingerprint sensor properties relative to each display.
          Note: this value is temporary and is expected to be queried directly
          from the HAL in the future. -->
@@ -6210,4 +6235,27 @@
         trusted certificate using the SHA-256 digest algorithm. -->
     <string-array name="config_healthConnectMigrationKnownSigners">
     </string-array>
+
+    <!-- The Universal Stylus Initiative (USI) protocol version supported by each display.
+         (@see https://universalstylus.org/).
+
+         The i-th value in this array corresponds to the supported USI version of the i-th display
+         listed in config_displayUniqueIdArray. On a single-display device, the
+         config_displayUniqueIdArray may be empty, in which case the only value in this array should
+         be the USI version for the main built-in display.
+
+         If the display does not support USI, the version value should be an empty string. If the
+         display supports USI, the version must be in the following format:
+           "<major-version>.<minor-version>"
+
+         For example, "", "1.0", and "2.0" are valid values. -->
+    <string-array name="config_displayUsiVersionArray" translatable="false">
+        <item>""</item>
+    </string-array>
+
+    <!-- Whether system apps should be scanned in the stopped state during initial boot.
+        Packages can be added by OEMs in an allowlist, to prevent them from being scanned as
+        "stopped" during initial boot of a device, or after an OTA update. Stopped state of
+        an app is not changed during subsequent reboots.  -->
+    <bool name="config_stopSystemPackagesByDefault">false</bool>
 </resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index e330ee3..da44c2e 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5067,6 +5067,13 @@
     <string name="managed_profile_label_badge_2">2nd Work <xliff:g id="label" example="Email">%1$s</xliff:g></string>
     <string name="managed_profile_label_badge_3">3rd Work <xliff:g id="label" example="Email">%1$s</xliff:g></string>
 
+    <!--
+        Used to wrap a label for content description for a Clone profile, e.g. "Clone Messenger"
+        instead of Messenger when the Messenger app is cloned.
+        [CHAR LIMIT=20]
+    -->
+    <string name="clone_profile_label_badge">Clone <xliff:g id="label" example="Messenger">%1$s</xliff:g></string>
+
     <!-- DO NOT TRANSLATE -->
     <string name="time_placeholder">--</string>
 
@@ -6234,4 +6241,17 @@
     <string name="mic_access_on_toast">Microphone is available</string>
     <!-- Toast message that is shown when the user mutes the microphone from the keyboard. [CHAR LIMIT=TOAST] -->
     <string name="mic_access_off_toast">Microphone is blocked</string>
+
+    <!-- Name of concurrent display notifications. [CHAR LIMIT=NONE] -->
+    <string name="concurrent_display_notification_name">Dual screen</string>
+    <!-- Title of concurrent display active notification. [CHAR LIMIT=NONE] -->
+    <string name="concurrent_display_notification_active_title">Dual screen is on</string>
+    <!-- Content of concurrent display active notification. [CHAR LIMIT=NONE] -->
+    <string name="concurrent_display_notification_active_content"><xliff:g id="app_name" example="Camera app">%1$s</xliff:g> is using both displays to show content</string>
+    <!-- Title of concurrent display thermal notification. [CHAR LIMIT=NONE] -->
+    <string name="concurrent_display_notification_thermal_title">Device is too warm</string>
+    <!-- Content of concurrent display thermal notification. [CHAR LIMIT=NONE] -->
+    <string name="concurrent_display_notification_thermal_content">Dual Screen is unavailable because your phone is getting too warm</string>
+    <!-- Text of device state notification turn off button. [CHAR LIMIT=NONE] -->
+    <string name="device_state_notification_turn_off_button">Turn off</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 76af814..2af91e1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -473,6 +473,7 @@
   <java-symbol type="integer" name="config_multiuserMaxRunningUsers" />
   <java-symbol type="bool" name="config_multiuserDelayUserDataLocking" />
   <java-symbol type="bool" name="config_multiuserVisibleBackgroundUsers" />
+  <java-symbol type="bool" name="config_multiuserVisibleBackgroundUsersOnDefaultDisplay" />
   <java-symbol type="bool" name="config_enableTimeoutToDockUserWhenDocked" />
   <java-symbol type="integer" name="config_userTypePackageWhitelistMode"/>
   <java-symbol type="xml" name="config_user_types" />
@@ -1063,6 +1064,7 @@
   <java-symbol type="string" name="managed_profile_label_badge" />
   <java-symbol type="string" name="managed_profile_label_badge_2" />
   <java-symbol type="string" name="managed_profile_label_badge_3" />
+  <java-symbol type="string" name="clone_profile_label_badge" />
   <java-symbol type="string" name="mediasize_unknown_portrait" />
   <java-symbol type="string" name="mediasize_unknown_landscape" />
   <java-symbol type="string" name="mediasize_iso_a0" />
@@ -1386,6 +1388,8 @@
   <java-symbol type="drawable" name="ic_qs_auto_rotate" />
   <java-symbol type="drawable" name="ic_qs_dnd" />
   <java-symbol type="drawable" name="ic_qs_one_handed_mode" />
+  <java-symbol type="drawable" name="ic_clone_icon_badge" />
+  <java-symbol type="drawable" name="ic_clone_badge" />
 
   <java-symbol type="drawable" name="sim_light_blue" />
   <java-symbol type="drawable" name="sim_light_green" />
@@ -2646,6 +2650,8 @@
   <java-symbol type="array" name="config_biometric_sensors" />
   <java-symbol type="bool" name="allow_test_udfps" />
   <java-symbol type="array" name="config_udfps_sensor_props" />
+  <java-symbol type="array" name="config_udfps_touch_detection_options" />
+  <java-symbol type="integer" name="config_selected_udfps_touch_detection" />
   <java-symbol type="array" name="config_sfps_sensor_props" />
   <java-symbol type="bool" name="config_is_powerbutton_fps" />
   <java-symbol type="array" name="config_udfps_enroll_stage_thresholds" />
@@ -4874,6 +4880,18 @@
   <java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" />
   <java-symbol type="array" name="config_serviceStateLocationAllowedPackages" />
   <java-symbol type="integer" name="config_deviceStateRearDisplay"/>
+  <java-symbol type="array" name="device_state_notification_state_identifiers"/>
+  <java-symbol type="array" name="device_state_notification_names"/>
+  <java-symbol type="array" name="device_state_notification_active_titles"/>
+  <java-symbol type="array" name="device_state_notification_active_contents"/>
+  <java-symbol type="array" name="device_state_notification_thermal_titles"/>
+  <java-symbol type="array" name="device_state_notification_thermal_contents"/>
+  <java-symbol type="string" name="concurrent_display_notification_name"/>
+  <java-symbol type="string" name="concurrent_display_notification_active_title"/>
+  <java-symbol type="string" name="concurrent_display_notification_active_content"/>
+  <java-symbol type="string" name="concurrent_display_notification_thermal_title"/>
+  <java-symbol type="string" name="concurrent_display_notification_thermal_content"/>
+  <java-symbol type="string" name="device_state_notification_turn_off_button"/>
   <java-symbol type="bool" name="config_independentLockscreenLiveWallpaper"/>
 
   <!-- For app language picker -->
@@ -4892,4 +4910,6 @@
   <java-symbol type="string" name="config_mainDisplayShape"/>
   <java-symbol type="string" name="config_secondaryDisplayShape"/>
   <java-symbol type="array" name="config_displayShapeArray" />
+
+  <java-symbol type="bool" name="config_stopSystemPackagesByDefault"/>
 </resources>
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index d0c3e5f..f233c6e 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -36,6 +36,12 @@
 }
 
 prebuilt_etc {
+    name: "initial-package-stopped-states.xml",
+    sub_dir: "sysconfig",
+    src: "initial-package-stopped-states.xml",
+}
+
+prebuilt_etc {
     name: "preinstalled-packages-platform-overlays.xml",
     product_specific: true,
     sub_dir: "sysconfig",
diff --git a/data/etc/initial-package-stopped-states.xml b/data/etc/initial-package-stopped-states.xml
new file mode 100644
index 0000000..6bda2c0
--- /dev/null
+++ b/data/etc/initial-package-stopped-states.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+ -->
+
+<!--
+This XML defines an allowlist for packages that should not be scanned in a "stopped" state.
+When this feature is turned on (indicated by the config config_stopSystemPackagesByDefault in
+core/res/res/values/config.xml) packages on the system partition that are encountered by
+the PackageManagerService for the first time are scanned in the "stopped" state. This allowlist
+is also considered while creating new users on the device. Stopped state is not set during
+subsequent reboots.
+
+Example usage
+    1. <initial-package-state package="com.example.app" stopped="false"/>
+        Indicates that a system package - com.example.app's initial stopped state should not be set
+        by the Package Manager. By default, system apps are marked as stopped.
+    2. <initial-package-state package="com.example.app" stopped="true"/>
+        Indicates that a system package - com.example.app's initial state should be set by the
+        Package Manager to "stopped=true". It will have the same effect on the
+        package's stopped state even if this package was not included in the allow list.
+    3. <initial-package-state package="com.example.app"/>
+        Invalid usage.
+-->
+
+<config></config>
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index 2830d7eff..4715045 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -801,25 +801,32 @@
             ));
 
             if (mSpec.isDevicePropertiesAttestationIncluded()) {
+                final String platformReportedBrand = TextUtils.isEmpty(Build.BRAND_FOR_ATTESTATION)
+                        ? Build.BRAND : Build.BRAND_FOR_ATTESTATION;
                 params.add(KeyStore2ParameterUtils.makeBytes(
                         KeymasterDefs.KM_TAG_ATTESTATION_ID_BRAND,
-                        Build.BRAND.getBytes(StandardCharsets.UTF_8)
+                        platformReportedBrand.getBytes(StandardCharsets.UTF_8)
                 ));
                 params.add(KeyStore2ParameterUtils.makeBytes(
                         KeymasterDefs.KM_TAG_ATTESTATION_ID_DEVICE,
                         Build.DEVICE.getBytes(StandardCharsets.UTF_8)
                 ));
+                final String platformReportedProduct =
+                        TextUtils.isEmpty(Build.PRODUCT_FOR_ATTESTATION) ? Build.PRODUCT :
+                                Build.PRODUCT_FOR_ATTESTATION;
                 params.add(KeyStore2ParameterUtils.makeBytes(
                         KeymasterDefs.KM_TAG_ATTESTATION_ID_PRODUCT,
-                        Build.PRODUCT.getBytes(StandardCharsets.UTF_8)
+                        platformReportedProduct.getBytes(StandardCharsets.UTF_8)
                 ));
                 params.add(KeyStore2ParameterUtils.makeBytes(
                         KeymasterDefs.KM_TAG_ATTESTATION_ID_MANUFACTURER,
                         Build.MANUFACTURER.getBytes(StandardCharsets.UTF_8)
                 ));
+                final String platformReportedModel = TextUtils.isEmpty(Build.MODEL_FOR_ATTESTATION)
+                        ? Build.MODEL : Build.MODEL_FOR_ATTESTATION;
                 params.add(KeyStore2ParameterUtils.makeBytes(
                         KeymasterDefs.KM_TAG_ATTESTATION_ID_MODEL,
-                        Build.MODEL.getBytes(StandardCharsets.UTF_8)
+                        platformReportedModel.getBytes(StandardCharsets.UTF_8)
                 ));
             }
 
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index c0f3086..13a695a 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -180,8 +180,6 @@
     ATRACE_CALL();
 
     if (window) {
-        // Ensure the hint session is running here, away from any critical paths
-        mHintSessionWrapper.init();
         mNativeSurface = std::make_unique<ReliableSurface>(window);
         mNativeSurface->init();
         if (enableTimeout) {
@@ -1050,6 +1048,10 @@
     mSyncDelayDuration = duration;
 }
 
+void CanvasContext::startHintSession() {
+    mHintSessionWrapper.init();
+}
+
 } /* namespace renderthread */
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 0f6b736..deb2109 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -224,6 +224,8 @@
 
     void setSyncDelayDuration(nsecs_t duration);
 
+    void startHintSession();
+
 private:
     CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
                   IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline,
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index f8e2dee..ce1a551 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -45,8 +45,12 @@
     pid_t uiThreadId = pthread_gettid_np(pthread_self());
     pid_t renderThreadId = getRenderThreadTid();
     mContext = mRenderThread.queue().runSync([=, this]() -> CanvasContext* {
-        return CanvasContext::create(mRenderThread, translucent, rootRenderNode, contextFactory,
-                                     uiThreadId, renderThreadId);
+        CanvasContext* context = CanvasContext::create(mRenderThread, translucent, rootRenderNode,
+                                                       contextFactory, uiThreadId, renderThreadId);
+        if (context != nullptr) {
+            mRenderThread.queue().post([=] { context->startHintSession(); });
+        }
+        return context;
     });
     mDrawFrameTask.setContext(&mRenderThread, mContext, rootRenderNode);
 }
diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java
index ff0a492..96117ef 100644
--- a/media/java/android/media/RouteListingPreference.java
+++ b/media/java/android/media/RouteListingPreference.java
@@ -17,7 +17,6 @@
 package android.media;
 
 import android.annotation.IntDef;
-import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
@@ -45,14 +44,24 @@
 public final class RouteListingPreference implements Parcelable {
 
     /**
-     * {@link Intent} action for apps to take the user to a screen for transferring media playback
-     * to the route with the id provided by the extra with key {@link #EXTRA_ROUTE_ID}.
+     * {@link Intent} action that the system uses to take the user the app when the user selects an
+     * {@link Item} whose {@link Item#getSelectionBehavior() selection behavior} is {@link
+     * Item#SELECTION_BEHAVIOR_GO_TO_APP}.
+     *
+     * <p>The launched intent will identify the selected item using the extra identified by {@link
+     * #EXTRA_ROUTE_ID}.
+     *
+     * @see #getLinkedItemComponentName()
+     * @see Item#SELECTION_BEHAVIOR_GO_TO_APP
      */
     public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";
 
     /**
      * {@link Intent} string extra key that contains the {@link Item#getRouteId() id} of the route
      * to transfer to, as part of an {@link #ACTION_TRANSFER_MEDIA} intent.
+     *
+     * @see #getLinkedItemComponentName()
+     * @see Item#SELECTION_BEHAVIOR_GO_TO_APP
      */
     public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID";
 
@@ -72,12 +81,12 @@
 
     @NonNull private final List<Item> mItems;
     private final boolean mUseSystemOrdering;
-    @Nullable private final ComponentName mInAppOnlyItemRoutingReceiver;
+    @Nullable private final ComponentName mLinkedItemComponentName;
 
     private RouteListingPreference(Builder builder) {
         mItems = builder.mItems;
         mUseSystemOrdering = builder.mUseSystemOrdering;
-        mInAppOnlyItemRoutingReceiver = builder.mInAppOnlyItemRoutingReceiver;
+        mLinkedItemComponentName = builder.mLinkedItemComponentName;
     }
 
     private RouteListingPreference(Parcel in) {
@@ -85,7 +94,7 @@
                 in.readParcelableList(new ArrayList<>(), Item.class.getClassLoader(), Item.class);
         mItems = List.copyOf(items);
         mUseSystemOrdering = in.readBoolean();
-        mInAppOnlyItemRoutingReceiver = ComponentName.readFromParcel(in);
+        mLinkedItemComponentName = ComponentName.readFromParcel(in);
     }
 
     /**
@@ -110,18 +119,20 @@
     }
 
     /**
-     * Returns a {@link ComponentName} for handling routes disabled via {@link
-     * Item#DISABLE_REASON_IN_APP_ONLY}, or null if the user needs to manually navigate to the app
-     * in order to route to select the corresponding routes.
+     * Returns a {@link ComponentName} for navigating to the application.
      *
-     * <p>If the user selects an {@link Item} disabled via {@link Item#DISABLE_REASON_IN_APP_ONLY},
-     * and this method returns a non-null {@link ComponentName}, the system takes the user back to
-     * the app by launching an intent to the returned {@link ComponentName}, using action {@link
-     * #ACTION_TRANSFER_MEDIA}, with the extra {@link #EXTRA_ROUTE_ID}.
+     * <p>Must not be null if any of the {@link #getItems() items} of this route listing preference
+     * has {@link Item#getSelectionBehavior() selection behavior} {@link
+     * Item#SELECTION_BEHAVIOR_GO_TO_APP}.
+     *
+     * <p>The system navigates to the application when the user selects {@link Item} with {@link
+     * Item#SELECTION_BEHAVIOR_GO_TO_APP} by launching an intent to the returned {@link
+     * ComponentName}, using action {@link #ACTION_TRANSFER_MEDIA}, with the extra {@link
+     * #EXTRA_ROUTE_ID}.
      */
     @Nullable
-    public ComponentName getInAppOnlyItemRoutingReceiver() {
-        return mInAppOnlyItemRoutingReceiver;
+    public ComponentName getLinkedItemComponentName() {
+        return mLinkedItemComponentName;
     }
 
     // RouteListingPreference Parcelable implementation.
@@ -135,7 +146,7 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeParcelableList(mItems, flags);
         dest.writeBoolean(mUseSystemOrdering);
-        ComponentName.writeToParcel(mInAppOnlyItemRoutingReceiver, dest);
+        ComponentName.writeToParcel(mLinkedItemComponentName, dest);
     }
 
     // Equals and hashCode.
@@ -151,13 +162,12 @@
         RouteListingPreference that = (RouteListingPreference) other;
         return mItems.equals(that.mItems)
                 && mUseSystemOrdering == that.mUseSystemOrdering
-                && Objects.equals(
-                        mInAppOnlyItemRoutingReceiver, that.mInAppOnlyItemRoutingReceiver);
+                && Objects.equals(mLinkedItemComponentName, that.mLinkedItemComponentName);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mItems, mUseSystemOrdering, mInAppOnlyItemRoutingReceiver);
+        return Objects.hash(mItems, mUseSystemOrdering, mLinkedItemComponentName);
     }
 
     /** Builder for {@link RouteListingPreference}. */
@@ -165,7 +175,7 @@
 
         private List<Item> mItems;
         private boolean mUseSystemOrdering;
-        private ComponentName mInAppOnlyItemRoutingReceiver;
+        private ComponentName mLinkedItemComponentName;
 
         /** Creates a new instance with default values (documented in the setters). */
         public Builder() {
@@ -198,14 +208,13 @@
         }
 
         /**
-         * See {@link #getInAppOnlyItemRoutingReceiver()}.
+         * See {@link #getLinkedItemComponentName()}.
          *
          * <p>The default value is {@code null}.
          */
         @NonNull
-        public Builder setInAppOnlyItemRoutingReceiver(
-                @Nullable ComponentName inAppOnlyItemRoutingReceiver) {
-            mInAppOnlyItemRoutingReceiver = inAppOnlyItemRoutingReceiver;
+        public Builder setLinkedItemComponentName(@Nullable ComponentName linkedItemComponentName) {
+            mLinkedItemComponentName = linkedItemComponentName;
             return this;
         }
 
@@ -225,6 +234,29 @@
         /** @hide */
         @Retention(RetentionPolicy.SOURCE)
         @IntDef(
+                prefix = {"SELECTION_BEHAVIOR_"},
+                value = {
+                    SELECTION_BEHAVIOR_NONE,
+                    SELECTION_BEHAVIOR_TRANSFER,
+                    SELECTION_BEHAVIOR_GO_TO_APP
+                })
+        public @interface SelectionBehavior {}
+
+        /** The corresponding route is not selectable by the user. */
+        public static final int SELECTION_BEHAVIOR_NONE = 0;
+        /** If the user selects the corresponding route, the media transfers to the said route. */
+        public static final int SELECTION_BEHAVIOR_TRANSFER = 1;
+        /**
+         * If the user selects the corresponding route, the system takes the user to the
+         * application.
+         *
+         * <p>The system uses {@link #getLinkedItemComponentName()} in order to navigate to the app.
+         */
+        public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(
                 flag = true,
                 prefix = {"FLAG_"},
                 value = {FLAG_ONGOING_SESSION, FLAG_SUGGESTED_ROUTE})
@@ -251,46 +283,40 @@
         @IntDef(
                 prefix = {"DISABLE_REASON_"},
                 value = {
-                    DISABLE_REASON_NONE,
-                    DISABLE_REASON_SUBSCRIPTION_REQUIRED,
-                    DISABLE_REASON_DOWNLOADED_CONTENT,
-                    DISABLE_REASON_AD,
-                    DISABLE_REASON_IN_APP_ONLY,
-                    DISABLE_REASON_CUSTOM
+                    SUBTEXT_NONE,
+                    SUBTEXT_SUBSCRIPTION_REQUIRED,
+                    SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED,
+                    SUBTEXT_AD_ROUTING_DISALLOWED,
+                    SUBTEXT_CUSTOM
                 })
-        public @interface DisableReason {}
+        public @interface SubText {}
 
-        /** The corresponding route is available for routing. */
-        public static final int DISABLE_REASON_NONE = 0;
+        /** The corresponding route has no associated subtext. */
+        public static final int SUBTEXT_NONE = 0;
         /**
-         * The corresponding route requires a special subscription in order to be available for
-         * routing.
+         * The corresponding route's subtext must indicate that it requires a special subscription
+         * in order to be available for routing.
          */
-        public static final int DISABLE_REASON_SUBSCRIPTION_REQUIRED = 1;
+        public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 1;
         /**
-         * The corresponding route is not available because downloaded content cannot be routed to
-         * it.
+         * The corresponding route's subtext must indicate that downloaded content cannot be routed
+         * to it.
          */
-        public static final int DISABLE_REASON_DOWNLOADED_CONTENT = 2;
-        /** The corresponding route is not available because an ad is in progress. */
-        public static final int DISABLE_REASON_AD = 3;
+        public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 2;
         /**
-         * The corresponding route is only available for routing from within the app.
-         *
-         * <p>The user may still select the corresponding route if the app provides an {@link
-         * #getInAppOnlyItemRoutingReceiver() in-app routing receiver}, in which case the system
-         * will take the user to the app.
+         * The corresponding route's subtext must indicate that it is not available because an ad is
+         * in progress.
          */
-        public static final int DISABLE_REASON_IN_APP_ONLY = 4;
+        public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 3;
         /**
-         * The corresponding route is not available because of the reason described by {@link
-         * #getCustomDisableReasonMessage()}.
+         * The corresponding route's subtext must be obtained from {@link
+         * #getCustomSubtextMessage()}.
          *
          * <p>Applications should strongly prefer one of the other disable reasons (for the full
-         * list, see {@link #getDisableReason()}) in order to guarantee correct localization and
-         * rendering across all form factors.
+         * list, see {@link #getSubText()}) in order to guarantee correct localization and rendering
+         * across all form factors.
          */
-        public static final int DISABLE_REASON_CUSTOM = 5;
+        public static final int SUBTEXT_CUSTOM = 10000;
 
         @NonNull
         public static final Creator<Item> CREATOR =
@@ -307,29 +333,27 @@
                 };
 
         @NonNull private final String mRouteId;
+        @SelectionBehavior private final int mSelectionBehavior;
         @Flags private final int mFlags;
-        @DisableReason private final int mDisableReason;
-        private final int mSessionParticipantCount;
-        @Nullable private final CharSequence mCustomDisableReasonMessage;
+        @SubText private final int mSubText;
+
+        @Nullable private final CharSequence mCustomSubtextMessage;
 
         private Item(@NonNull Builder builder) {
             mRouteId = builder.mRouteId;
+            mSelectionBehavior = builder.mSelectionBehavior;
             mFlags = builder.mFlags;
-            mDisableReason = builder.mDisableReason;
-            mSessionParticipantCount = builder.mSessionParticipantCount;
-            mCustomDisableReasonMessage = builder.mCustomDisableReasonMessage;
-            validateCustomDisableReasonMessage();
+            mSubText = builder.mSubText;
+            mCustomSubtextMessage = builder.mCustomSubtextMessage;
         }
 
         private Item(Parcel in) {
             mRouteId = in.readString();
             Preconditions.checkArgument(!TextUtils.isEmpty(mRouteId));
+            mSelectionBehavior = in.readInt();
             mFlags = in.readInt();
-            mDisableReason = in.readInt();
-            mSessionParticipantCount = in.readInt();
-            Preconditions.checkArgument(mSessionParticipantCount >= 0);
-            mCustomDisableReasonMessage = in.readCharSequence();
-            validateCustomDisableReasonMessage();
+            mSubText = in.readInt();
+            mCustomSubtextMessage = in.readCharSequence();
         }
 
         /**
@@ -343,6 +367,17 @@
         }
 
         /**
+         * Returns the behavior that the corresponding route has if the user selects it.
+         *
+         * @see #SELECTION_BEHAVIOR_NONE
+         * @see #SELECTION_BEHAVIOR_TRANSFER
+         * @see #SELECTION_BEHAVIOR_GO_TO_APP
+         */
+        public int getSelectionBehavior() {
+            return mSelectionBehavior;
+        }
+
+        /**
          * Returns the flags associated to the route that corresponds to this item.
          *
          * @see #FLAG_ONGOING_SESSION
@@ -354,49 +389,42 @@
         }
 
         /**
-         * Returns the reason for the corresponding route to be disabled, or {@link
-         * #DISABLE_REASON_NONE} if the route is not disabled.
+         * Returns the type of subtext associated to this route.
          *
-         * @see #DISABLE_REASON_NONE
-         * @see #DISABLE_REASON_SUBSCRIPTION_REQUIRED
-         * @see #DISABLE_REASON_DOWNLOADED_CONTENT
-         * @see #DISABLE_REASON_AD
-         * @see #DISABLE_REASON_IN_APP_ONLY
-         * @see #DISABLE_REASON_CUSTOM
+         * <p>Subtext types other than {@link #SUBTEXT_NONE} and {@link #SUBTEXT_CUSTOM} must not
+         * have {@link #SELECTION_BEHAVIOR_TRANSFER}.
+         *
+         * <p>If this method returns {@link #SUBTEXT_CUSTOM}, then the subtext is obtained form
+         * {@link #getCustomSubtextMessage()}.
+         *
+         * @see #SUBTEXT_NONE
+         * @see #SUBTEXT_SUBSCRIPTION_REQUIRED
+         * @see #SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED
+         * @see #SUBTEXT_AD_ROUTING_DISALLOWED
+         * @see #SUBTEXT_CUSTOM
          */
-        @DisableReason
-        public int getDisableReason() {
-            return mDisableReason;
+        @SubText
+        public int getSubText() {
+            return mSubText;
         }
 
         /**
-         * Returns a non-negative number of participants in the ongoing session (if any) on the
-         * corresponding route.
+         * Returns a human-readable {@link CharSequence} providing the subtext for the corresponding
+         * route.
          *
-         * <p>The system ignores this value if zero, or if {@link #getFlags()} does not include
-         * {@link #FLAG_ONGOING_SESSION}.
-         */
-        public int getSessionParticipantCount() {
-            return mSessionParticipantCount;
-        }
-
-        /**
-         * Returns a human-readable {@link CharSequence} describing the reason for this route to be
-         * disabled. May be null if {@link #getDisableReason()} is not {@link
-         * #DISABLE_REASON_CUSTOM}.
-         *
-         * <p>This value is ignored if the {@link #getDisableReason() disable reason} for this item
-         * is not {@link #DISABLE_REASON_CUSTOM}.
+         * <p>This value is ignored if the {@link #getSubText() subtext} for this item is not {@link
+         * #SUBTEXT_CUSTOM}..
          *
          * <p>Applications must provide a localized message that matches the system's locale. See
          * {@link Locale#getDefault()}.
          *
-         * <p>This message is a hint for the system. Applications should strongly prefer one of the
-         * other disable reasons listed in {@link #getDisableReason()}.
+         * <p>Applications should avoid using custom messages (and instead use one of non-custom
+         * subtexts listed in {@link #getSubText()} in order to guarantee correct visual
+         * representation and localization on all form factors.
          */
         @Nullable
-        public CharSequence getCustomDisableReasonMessage() {
-            return mCustomDisableReasonMessage;
+        public CharSequence getCustomSubtextMessage() {
+            return mCustomSubtextMessage;
         }
 
         // Item Parcelable implementation.
@@ -409,10 +437,10 @@
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             dest.writeString(mRouteId);
+            dest.writeInt(mSelectionBehavior);
             dest.writeInt(mFlags);
-            dest.writeInt(mDisableReason);
-            dest.writeInt(mSessionParticipantCount);
-            dest.writeCharSequence(mCustomDisableReasonMessage);
+            dest.writeInt(mSubText);
+            dest.writeCharSequence(mCustomSubtextMessage);
         }
 
         // Equals and hashCode.
@@ -427,40 +455,26 @@
             }
             Item item = (Item) other;
             return mRouteId.equals(item.mRouteId)
+                    && mSelectionBehavior == item.mSelectionBehavior
                     && mFlags == item.mFlags
-                    && mDisableReason == item.mDisableReason
-                    && mSessionParticipantCount == item.mSessionParticipantCount
-                    && TextUtils.equals(
-                            mCustomDisableReasonMessage, item.mCustomDisableReasonMessage);
+                    && mSubText == item.mSubText
+                    && TextUtils.equals(mCustomSubtextMessage, item.mCustomSubtextMessage);
         }
 
         @Override
         public int hashCode() {
             return Objects.hash(
-                    mRouteId,
-                    mFlags,
-                    mDisableReason,
-                    mSessionParticipantCount,
-                    mCustomDisableReasonMessage);
-        }
-
-        private void validateCustomDisableReasonMessage() {
-            if (mDisableReason == DISABLE_REASON_CUSTOM) {
-                Preconditions.checkArgument(
-                        !TextUtils.isEmpty(mCustomDisableReasonMessage),
-                        "customDisableReasonMessage must not be null or empty if disable reason is"
-                                + " DISABLE_REASON_CUSTOM.");
-            }
+                    mRouteId, mSelectionBehavior, mFlags, mSubText, mCustomSubtextMessage);
         }
 
         /** Builder for {@link Item}. */
         public static final class Builder {
 
             private final String mRouteId;
+            private int mSelectionBehavior;
             private int mFlags;
-            private int mDisableReason;
-            private int mSessionParticipantCount;
-            private CharSequence mCustomDisableReasonMessage;
+            private int mSubText;
+            private CharSequence mCustomSubtextMessage;
 
             /**
              * Constructor.
@@ -470,39 +484,51 @@
             public Builder(@NonNull String routeId) {
                 Preconditions.checkArgument(!TextUtils.isEmpty(routeId));
                 mRouteId = routeId;
-                mDisableReason = DISABLE_REASON_NONE;
+                mSelectionBehavior = SELECTION_BEHAVIOR_TRANSFER;
+                mSubText = SUBTEXT_NONE;
             }
 
-            /** See {@link Item#getFlags()}. */
+            /**
+             * See {@link Item#getSelectionBehavior()}.
+             *
+             * <p>The default value is {@link #ACTION_TRANSFER_MEDIA}.
+             */
+            @NonNull
+            public Builder setSelectionBehavior(int selectionBehavior) {
+                mSelectionBehavior = selectionBehavior;
+                return this;
+            }
+
+            /**
+             * See {@link Item#getFlags()}.
+             *
+             * <p>The default value is zero (no flags).
+             */
             @NonNull
             public Builder setFlags(int flags) {
                 mFlags = flags;
                 return this;
             }
 
-            /** See {@link Item#getDisableReason()}. */
+            /**
+             * See {@link Item#getSubText()}.
+             *
+             * <p>The default value is {@link #SUBTEXT_NONE}.
+             */
             @NonNull
-            public Builder setDisableReason(int disableReason) {
-                mDisableReason = disableReason;
+            public Builder setSubText(int subText) {
+                mSubText = subText;
                 return this;
             }
 
-            /** See {@link Item#getSessionParticipantCount()}. */
+            /**
+             * See {@link Item#getCustomSubtextMessage()}.
+             *
+             * <p>The default value is {@code null}.
+             */
             @NonNull
-            public Builder setSessionParticipantCount(
-                    @IntRange(from = 0) int sessionParticipantCount) {
-                Preconditions.checkArgument(
-                        sessionParticipantCount >= 0,
-                        "sessionParticipantCount must be non-negative.");
-                mSessionParticipantCount = sessionParticipantCount;
-                return this;
-            }
-
-            /** See {@link Item#getCustomDisableReasonMessage()}. */
-            @NonNull
-            public Builder setCustomDisableReasonMessage(
-                    @Nullable CharSequence customDisableReasonMessage) {
-                mCustomDisableReasonMessage = customDisableReasonMessage;
+            public Builder setCustomSubtextMessage(@Nullable CharSequence customSubtextMessage) {
+                mCustomSubtextMessage = customSubtextMessage;
                 return this;
             }
 
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index ae6f71c..ab680b0 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -38,10 +38,11 @@
     <!-- Message for updating an existing app [CHAR LIMIT=NONE] -->
     <string name="install_confirm_question_update">Do you want to update this app?</string>
     <!-- TODO(b/244413073) Revise the description after getting UX input and UXR on this. -->
-    <!-- Message for updating an existing app when updating owner changed [CHAR LIMIT=NONE] -->
+    <!-- Message for updating an existing app when updating owner changed [DO NOT TRANSLATE][CHAR LIMIT=NONE] -->
     <string name="install_confirm_question_update_owner_changed">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nBy updating, you\'ll get future updates from <xliff:g id="new_update_owner">%2$s</xliff:g> instead.</string>
-    <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] -->
-    <string name="install_confirm_question_update_owner_reminder">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nDo you want to install this update from <xliff:g id="new_update_owner">%2$s</xliff:g>.</string>
+    <!-- TODO(b/244413073) Revise the description after getting UX input and UXR on this. -->
+    <!-- Message for updating an existing app with update owner reminder [DO NOT TRANSLATE][CHAR LIMIT=NONE] -->
+    <string name="install_confirm_question_update_owner_reminder">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nDo you want to install this update from <xliff:g id="new_update_owner">%2$s</xliff:g>?</string>
     <!-- [CHAR LIMIT=100] -->
     <string name="install_failed">App not installed.</string>
     <!-- Reason displayed when installation fails because the package was blocked
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index d708e57..17ef283 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -30,8 +30,8 @@
 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET;
 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
-import static android.media.RouteListingPreference.Item.DISABLE_REASON_NONE;
 import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED_ROUTE;
+import static android.media.RouteListingPreference.Item.SUBTEXT_NONE;
 
 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
 
@@ -198,10 +198,11 @@
      *
      * @return disabled reason of device
      */
-    @RouteListingPreference.Item.DisableReason
+    @RouteListingPreference.Item.SubText
     public int getDisableReason() {
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
-                ? mItem.getDisableReason() : -1;
+                ? mItem.getSubText()
+                : -1;
     }
 
     /**
@@ -210,8 +211,9 @@
      * @return true if device has disabled reason
      */
     public boolean hasDisabledReason() {
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && mItem != null
-                && mItem.getDisableReason() != DISABLE_REASON_NONE;
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+                && mItem != null
+                && mItem.getSubText() != SUBTEXT_NONE;
     }
 
     /**
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
new file mode 100644
index 0000000..bdd4869
--- /dev/null
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider.settings.backup;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+/** Settings that should not be restored when target device is a large screen
+ *  i.e. tablets and foldables in unfolded state
+ */
+public class LargeScreenSettings {
+    private static final float LARGE_SCREEN_MIN_DPS = 600;
+    private static final String LARGE_SCREEN_DO_NOT_RESTORE = "accelerometer_rotation";
+
+   /**
+    * Autorotation setting should not be restored when the target device is a large screen.
+    * (b/243489549)
+    */
+    public static boolean doNotRestoreIfLargeScreenSetting(String key, Context context) {
+        return isLargeScreen(context) && LARGE_SCREEN_DO_NOT_RESTORE.equals(key);
+    }
+
+    // copied from systemui/shared/...Utilities.java
+    // since we don't want to add compile time dependency on sys ui package
+    private static boolean isLargeScreen(Context context) {
+        final WindowManager windowManager = context.getSystemService(WindowManager.class);
+        final Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
+        float smallestWidth = dpiFromPx(Math.min(bounds.width(), bounds.height()),
+                context.getResources().getConfiguration().densityDpi);
+        return smallestWidth >= LARGE_SCREEN_MIN_DPS;
+    }
+
+    private static float dpiFromPx(float size, int densityDpi) {
+        float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
+        return (size / densityRatio);
+    }
+}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 6aa08f2..574fd5a 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -37,6 +37,7 @@
 import android.provider.Settings;
 import android.provider.settings.backup.DeviceSpecificSettings;
 import android.provider.settings.backup.GlobalSettings;
+import android.provider.settings.backup.LargeScreenSettings;
 import android.provider.settings.backup.SecureSettings;
 import android.provider.settings.backup.SystemSettings;
 import android.provider.settings.validators.GlobalSettingsValidators;
@@ -812,6 +813,12 @@
                 continue;
             }
 
+            if (LargeScreenSettings.doNotRestoreIfLargeScreenSetting(key, getBaseContext())) {
+                Log.i(TAG, "Skipping restore for setting " + key + " as the target device "
+                        + "is a large screen (i.e tablet or foldable in unfolded state)");
+                continue;
+            }
+
             String value = null;
             boolean hasValueToRestore = false;
             if (cachedEntries.indexOfKey(key) >= 0) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 25b9906..7aeba95 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -238,6 +238,15 @@
         dumpSetting(s, p,
                 Settings.Global.AUDIO_SAFE_VOLUME_STATE,
                 GlobalSettingsProto.AUDIO_SAFE_VOLUME_STATE);
+        dumpSetting(s, p,
+                Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE,
+                GlobalSettingsProto.AUDIO_SAFE_CSD_CURRENT_VALUE);
+        dumpSetting(s, p,
+                Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING,
+                GlobalSettingsProto.AUDIO_SAFE_CSD_NEXT_WARNING);
+        dumpSetting(s, p,
+                Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS,
+                GlobalSettingsProto.AUDIO_SAFE_CSD_DOSE_RECORDS);
 
         final long autofillToken = p.start(GlobalSettingsProto.AUTOFILL);
         dumpSetting(s, p,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 3782ce4..f1ea482 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -131,6 +131,9 @@
                     Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE,
                     Settings.Global.ASSISTED_GPS_ENABLED,
                     Settings.Global.AUDIO_SAFE_VOLUME_STATE,
+                    Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE,
+                    Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING,
+                    Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS,
                     Settings.Global.AUTOFILL_LOGGING_LEVEL,
                     Settings.Global.AUTOFILL_MAX_PARTITIONS_SIZE,
                     Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS,
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
index 0b1a3e2..46c604b 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -25,6 +25,7 @@
         "androidx.coordinatorlayout_coordinatorlayout",
         "androidx.core_core",
         "androidx.viewpager_viewpager",
+        "SettingsLib",
     ],
 
     uses_libs: [
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
index 26748a9..0b4d7cd 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml
@@ -16,6 +16,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.systemui.accessibility.accessibilitymenu">
+
+    <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS"/>
+
     <application>
         <service
             android:name="com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService"
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
index 76139c6..ecb2bf4 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
@@ -22,19 +22,28 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
+import android.hardware.display.BrightnessInfo;
 import android.hardware.display.DisplayManager;
+import android.media.AudioManager;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.provider.Settings;
 import android.view.Display;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
 
+import com.android.settingslib.display.BrightnessUtils;
+import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut.ShortcutId;
 import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuOverlayLayout;
 
+import java.util.List;
+
 /** @hide */
 public class AccessibilityMenuService extends AccessibilityService
         implements View.OnTouchListener {
@@ -42,6 +51,11 @@
 
     private static final long BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE = 100L;
 
+    private static final int BRIGHTNESS_UP_INCREMENT_GAMMA =
+            (int) Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f);
+    private static final int BRIGHTNESS_DOWN_INCREMENT_GAMMA =
+            (int) -Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f);
+
     private long mLastTimeTouchedOutside = 0L;
     // Timeout used to ignore the A11y button onClick() when ACTION_OUTSIDE is also received on
     // clicking on the A11y button.
@@ -51,6 +65,8 @@
 
     private static boolean sInitialized = false;
 
+    private AudioManager mAudioManager;
+
     // TODO(b/136716947): Support multi-display once a11y framework side is ready.
     private DisplayManager mDisplayManager;
     final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
@@ -140,6 +156,7 @@
 
         mDisplayManager = getSystemService(DisplayManager.class);
         mDisplayManager.registerDisplayListener(mDisplayListener, null);
+        mAudioManager = getSystemService(AudioManager.class);
 
         sInitialized = true;
     }
@@ -170,9 +187,93 @@
      * @param view the shortcut button being clicked.
      */
     public void handleClick(View view) {
+        // Shortcuts are repeatable in a11y menu rather than unique, so use tag ID to handle.
+        int viewTag = (int) view.getTag();
+
+        if (viewTag == ShortcutId.ID_ASSISTANT_VALUE.ordinal()) {
+            // Always restart the voice command activity, so that the UI is reloaded.
+            startActivityIfIntentIsSafe(
+                    new Intent(Intent.ACTION_VOICE_COMMAND),
+                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        } else if (viewTag == ShortcutId.ID_A11YSETTING_VALUE.ordinal()) {
+            startActivityIfIntentIsSafe(
+                    new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS),
+                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        } else if (viewTag == ShortcutId.ID_POWER_VALUE.ordinal()) {
+            performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
+        } else if (viewTag == ShortcutId.ID_RECENT_VALUE.ordinal()) {
+            performGlobalAction(GLOBAL_ACTION_RECENTS);
+        } else if (viewTag == ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()) {
+            performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN);
+        } else if (viewTag == ShortcutId.ID_QUICKSETTING_VALUE.ordinal()) {
+            performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS);
+        } else if (viewTag == ShortcutId.ID_NOTIFICATION_VALUE.ordinal()) {
+            performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS);
+        } else if (viewTag == ShortcutId.ID_SCREENSHOT_VALUE.ordinal()) {
+            performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT);
+        } else if (viewTag == ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal()) {
+            adjustBrightness(BRIGHTNESS_UP_INCREMENT_GAMMA);
+            return;
+        } else if (viewTag == ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal()) {
+            adjustBrightness(BRIGHTNESS_DOWN_INCREMENT_GAMMA);
+            return;
+        } else if (viewTag == ShortcutId.ID_VOLUME_UP_VALUE.ordinal()) {
+            adjustVolume(AudioManager.ADJUST_RAISE);
+            return;
+        } else if (viewTag == ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal()) {
+            adjustVolume(AudioManager.ADJUST_LOWER);
+            return;
+        }
+
         mA11yMenuLayout.hideMenu();
     }
 
+    private void adjustBrightness(int increment) {
+        BrightnessInfo info = getDisplay().getBrightnessInfo();
+        int gamma = BrightnessUtils.convertLinearToGammaFloat(
+                info.brightness,
+                info.brightnessMinimum,
+                info.brightnessMaximum
+        );
+        gamma = Math.max(
+                BrightnessUtils.GAMMA_SPACE_MIN,
+                Math.min(BrightnessUtils.GAMMA_SPACE_MAX, gamma + increment));
+
+        float brightness = BrightnessUtils.convertGammaToLinearFloat(
+                gamma,
+                info.brightnessMinimum,
+                info.brightnessMaximum
+        );
+        mDisplayManager.setTemporaryBrightness(getDisplayId(), brightness);
+        mDisplayManager.setBrightness(getDisplayId(), brightness);
+        mA11yMenuLayout.showSnackbar(
+                getString(R.string.brightness_percentage_label,
+                        (gamma / (BrightnessUtils.GAMMA_SPACE_MAX / 100))));
+    }
+
+    private void adjustVolume(int direction) {
+        mAudioManager.adjustStreamVolume(
+                AudioManager.STREAM_MUSIC, direction,
+                AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
+        final int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        final int volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+        mA11yMenuLayout.showSnackbar(
+                getString(
+                        R.string.music_volume_percentage_label,
+                        (int) (100.0 / maxVolume * volume))
+        );
+    }
+
+    private void startActivityIfIntentIsSafe(Intent intent, int flag) {
+        PackageManager packageManager = getPackageManager();
+        List<ResolveInfo> activities =
+                packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+        if (!activities.isEmpty()) {
+            intent.setFlags(flag);
+            startActivity(intent);
+        }
+    }
+
     @Override
     public void onInterrupt() {
     }
diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
index c181724..392d845 100644
--- a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
+++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
@@ -4,12 +4,16 @@
     android:id="@+id/work_profile_first_run"
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
+    android:paddingStart="16dp"
+    android:paddingEnd="4dp"
+    android:paddingVertical="16dp"
     android:visibility="gone">
     <ImageView
         android:id="@+id/screenshot_message_icon"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:paddingEnd="4dp"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_marginEnd="12dp"
+        android:layout_gravity="center_vertical"
         android:src="@drawable/ic_work_app_badge"/>
 
     <TextView
@@ -17,7 +21,11 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
-        android:layout_gravity="start"/>
+        android:layout_gravity="start|center_vertical"
+        android:textSize="18sp"
+        android:textColor="?android:attr/textColorPrimary"
+        android:lineHeight="24sp"
+        />
 
     <FrameLayout
         android:id="@+id/message_dismiss_button"
@@ -25,9 +33,9 @@
         android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
         android:contentDescription="@string/screenshot_dismiss_work_profile">
         <ImageView
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_margin="@dimen/overlay_dismiss_button_margin"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_gravity="center"
             android:src="@drawable/overlay_cancel"/>
     </FrameLayout>
 </LinearLayout>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 16bffb7..0f94628 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -240,7 +240,7 @@
     <!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
     <string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
     <!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] -->
-    <string name="screenshot_work_profile_notification">Work screenshots are saved in the <xliff:g id="app" example="Work Files">%1$s</xliff:g> app</string>
+    <string name="screenshot_work_profile_notification">Saved in <xliff:g id="app" example="Files">%1$s</xliff:g> in the work profile</string>
     <!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] -->
     <string name="screenshot_default_files_app_name">Files</string>
     <!-- A notice shown to the user to indicate that an app has detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 0685794..dd42c8e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -91,6 +91,7 @@
     private ViewGroup mStatusArea;
 
     // If the SMARTSPACE flag is set, keyguard_slice_view is replaced by the following views.
+    private ViewGroup mDateWeatherView;
     private View mWeatherView;
     private View mSmartspaceView;
 
@@ -201,7 +202,7 @@
             // TODO(b/261757708): add content observer for the Settings toggle and add/remove
             //  weather according to the Settings.
             if (mSmartspaceController.isDateWeatherDecoupled()) {
-                addWeatherView(viewIndex);
+                addDateWeatherView(viewIndex);
                 viewIndex += 1;
             }
 
@@ -239,6 +240,14 @@
 
     void onLocaleListChanged() {
         if (mSmartspaceController.isEnabled()) {
+            if (mSmartspaceController.isDateWeatherDecoupled()) {
+                mDateWeatherView.removeView(mWeatherView);
+                int index = mStatusArea.indexOfChild(mDateWeatherView);
+                if (index >= 0) {
+                    mStatusArea.removeView(mDateWeatherView);
+                    addDateWeatherView(index);
+                }
+            }
             int index = mStatusArea.indexOfChild(mSmartspaceView);
             if (index >= 0) {
                 mStatusArea.removeView(mSmartspaceView);
@@ -247,16 +256,28 @@
         }
     }
 
-    private void addWeatherView(int index) {
-        mWeatherView = mSmartspaceController.buildAndConnectWeatherView(mView);
+    private void addDateWeatherView(int index) {
+        mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView);
         LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                 MATCH_PARENT, WRAP_CONTENT);
-        mStatusArea.addView(mWeatherView, index, lp);
+        mStatusArea.addView(mDateWeatherView, index, lp);
         int startPadding = getContext().getResources().getDimensionPixelSize(
                 R.dimen.below_clock_padding_start);
         int endPadding = getContext().getResources().getDimensionPixelSize(
                 R.dimen.below_clock_padding_end);
-        mWeatherView.setPaddingRelative(startPadding, 0, endPadding, 0);
+        mDateWeatherView.setPaddingRelative(startPadding, 0, endPadding, 0);
+
+        addWeatherView();
+    }
+
+    private void addWeatherView() {
+        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                WRAP_CONTENT, WRAP_CONTENT);
+        mWeatherView = mSmartspaceController.buildAndConnectWeatherView(mView);
+        // Place weather right after the date, before the extras
+        final int index = mDateWeatherView.getChildCount() == 0 ? 0 : 1;
+        mDateWeatherView.addView(mWeatherView, index, lp);
+        mWeatherView.setPaddingRelative(0, 0, 4, 0);
     }
 
     private void addSmartspaceView(int index) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/EllipseOverlapDetectorParams.kt b/packages/SystemUI/src/com/android/systemui/biometrics/EllipseOverlapDetectorParams.kt
new file mode 100644
index 0000000..ab9b690
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/EllipseOverlapDetectorParams.kt
@@ -0,0 +1,13 @@
+package com.android.systemui.biometrics
+
+/**
+ * Collection of parameters used by EllipseOverlapDetector
+ *
+ * [minOverlap] minimum percentage (float from 0-1) needed to be considered a valid overlap
+ *
+ * [targetSize] percentage (defined as a float of 0-1) of sensor that is considered the target,
+ * expands outward from center
+ *
+ * [stepSize] size of each step when iterating over sensor pixel grid
+ */
+class EllipseOverlapDetectorParams(val minOverlap: Float, val targetSize: Float, val stepSize: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 7217f99..64d5518 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -557,7 +557,8 @@
         final InstanceId sessionIdProvider = mSessionTracker.getSessionId(
                 getBiometricSessionType());
         final int sessionId = (sessionIdProvider != null) ? sessionIdProvider.getId() : -1;
-        final int touchConfigId = BOUNDING_BOX_TOUCH_CONFIG_ID;
+        final int touchConfigId = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_selected_udfps_touch_detection);
 
         SysUiStatsLog.write(SysUiStatsLog.BIOMETRIC_TOUCH_REPORTED, biometricTouchReportedTouchType,
                 touchConfigId, sessionId, data.getX(), data.getY(), data.getMinor(),
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 6680787..45ca24d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -442,7 +442,8 @@
     ): WindowManager.LayoutParams {
         val paddingX = animation?.paddingX ?: 0
         val paddingY = animation?.paddingY ?: 0
-        if (animation != null && animation.listenForTouchesOutsideView()) {
+        if (!featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) && animation != null &&
+                animation.listenForTouchesOutsideView()) {
             flags = flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
index 001fed7..20c3e40 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
@@ -16,6 +16,9 @@
 
 package com.android.systemui.biometrics.dagger
 
+import android.content.res.Resources
+import com.android.internal.R
+import com.android.systemui.biometrics.EllipseOverlapDetectorParams
 import com.android.systemui.biometrics.udfps.BoundingBoxOverlapDetector
 import com.android.systemui.biometrics.udfps.EllipseOverlapDetector
 import com.android.systemui.biometrics.udfps.OverlapDetector
@@ -33,10 +36,30 @@
         @Provides
         @SysUISingleton
         fun providesOverlapDetector(featureFlags: FeatureFlags): OverlapDetector {
-            return if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) {
-                EllipseOverlapDetector()
+            if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) {
+                val selectedOption =
+                    Resources.getSystem()
+                        .getInteger(R.integer.config_selected_udfps_touch_detection)
+                val values =
+                    Resources.getSystem()
+                        .getStringArray(R.array.config_udfps_touch_detection_options)[
+                            selectedOption]
+                        .split(",")
+                        .map { it.toFloat() }
+
+                return if (values[0] == 1f) {
+                    EllipseOverlapDetector(
+                        EllipseOverlapDetectorParams(
+                            minOverlap = values[3],
+                            targetSize = values[2],
+                            stepSize = values[4].toInt()
+                        )
+                    )
+                } else {
+                    BoundingBoxOverlapDetector()
+                }
             } else {
-                BoundingBoxOverlapDetector()
+                return BoundingBoxOverlapDetector()
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
index 682d38a..2e2970f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
@@ -18,22 +18,84 @@
 
 import android.graphics.Point
 import android.graphics.Rect
-import androidx.annotation.VisibleForTesting
+import android.util.Log
+import com.android.systemui.biometrics.EllipseOverlapDetectorParams
 import com.android.systemui.dagger.SysUISingleton
 import kotlin.math.cos
-import kotlin.math.pow
 import kotlin.math.sin
 
+private enum class SensorPixelPosition {
+    OUTSIDE, // Pixel that falls outside of sensor circle
+    SENSOR, // Pixel within sensor circle
+    TARGET // Pixel within sensor center target
+}
+
+private val isDebug = true
+private val TAG = "EllipseOverlapDetector"
+
 /**
  * Approximates the touch as an ellipse and determines whether the ellipse has a sufficient overlap
  * with the sensor.
  */
 @SysUISingleton
-class EllipseOverlapDetector(private val neededPoints: Int = 2) : OverlapDetector {
-
+class EllipseOverlapDetector(private val params: EllipseOverlapDetectorParams) : OverlapDetector {
     override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean {
-        val points = calculateSensorPoints(nativeSensorBounds)
-        return points.count { checkPoint(it, touchData) } >= neededPoints
+        var isTargetTouched = false
+        var sensorPixels = 0
+        var coveredPixels = 0
+        for (y in nativeSensorBounds.top..nativeSensorBounds.bottom step params.stepSize) {
+            for (x in nativeSensorBounds.left..nativeSensorBounds.right step params.stepSize) {
+                // Check where pixel is within the sensor TODO: (b/265836919) This could be improved
+                // by precomputing these points
+                val pixelPosition =
+                    isPartOfSensorArea(
+                        x,
+                        y,
+                        nativeSensorBounds.centerX(),
+                        nativeSensorBounds.centerY(),
+                        nativeSensorBounds.width() / 2
+                    )
+                if (pixelPosition != SensorPixelPosition.OUTSIDE) {
+                    sensorPixels++
+
+                    // Check if this pixel falls within ellipse touch
+                    if (checkPoint(Point(x, y), touchData)) {
+                        coveredPixels++
+
+                        // Check that at least one covered pixel is within sensor target
+                        isTargetTouched =
+                            isTargetTouched or (pixelPosition == SensorPixelPosition.TARGET)
+                    }
+                }
+            }
+        }
+
+        val percentage: Float = coveredPixels.toFloat() / sensorPixels
+        if (isDebug) {
+            Log.v(
+                TAG,
+                "covered: $coveredPixels, sensor: $sensorPixels, " +
+                    "percentage: $percentage, isCenterTouched: $isTargetTouched"
+            )
+        }
+
+        return percentage >= params.minOverlap && isTargetTouched
+    }
+
+    /** Checks if point is in the sensor center target circle, outer circle, or outside of sensor */
+    private fun isPartOfSensorArea(x: Int, y: Int, cX: Int, cY: Int, r: Int): SensorPixelPosition {
+        val dx = cX - x
+        val dy = cY - y
+
+        val disSquared = dx * dx + dy * dy
+
+        return if (disSquared <= (r * params.targetSize) * (r * params.targetSize)) {
+            SensorPixelPosition.TARGET
+        } else if (disSquared <= r * r) {
+            SensorPixelPosition.SENSOR
+        } else {
+            SensorPixelPosition.OUTSIDE
+        }
     }
 
     private fun checkPoint(point: Point, touchData: NormalizedTouchData): Boolean {
@@ -45,29 +107,9 @@
         val c: Float = sin(touchData.orientation) * (point.x - touchData.x)
         val d: Float = cos(touchData.orientation) * (point.y - touchData.y)
         val result =
-            (a + b).pow(2) / (touchData.minor / 2).pow(2) +
-                (c - d).pow(2) / (touchData.major / 2).pow(2)
+            (a + b) * (a + b) / ((touchData.minor / 2) * (touchData.minor / 2)) +
+                (c - d) * (c - d) / ((touchData.major / 2) * (touchData.major / 2))
 
         return result <= 1
     }
-
-    @VisibleForTesting
-    fun calculateSensorPoints(sensorBounds: Rect): List<Point> {
-        val sensorX = sensorBounds.centerX()
-        val sensorY = sensorBounds.centerY()
-        val cornerOffset: Int = sensorBounds.width() / 4
-        val sideOffset: Int = sensorBounds.width() / 3
-
-        return listOf(
-            Point(sensorX - cornerOffset, sensorY - cornerOffset),
-            Point(sensorX, sensorY - sideOffset),
-            Point(sensorX + cornerOffset, sensorY - cornerOffset),
-            Point(sensorX - sideOffset, sensorY),
-            Point(sensorX, sensorY),
-            Point(sensorX + sideOffset, sensorY),
-            Point(sensorX - cornerOffset, sensorY + cornerOffset),
-            Point(sensorX, sensorY + sideOffset),
-            Point(sensorX + cornerOffset, sensorY + cornerOffset)
-        )
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index c3f24f0..45872f4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -218,6 +218,10 @@
     abstract BcSmartspaceConfigPlugin optionalBcSmartspaceConfigPlugin();
 
     @BindsOptionalOf
+    @Named(SmartspaceModule.DATE_SMARTSPACE_DATA_PLUGIN)
+    abstract BcSmartspaceDataPlugin optionalDateSmartspaceConfigPlugin();
+
+    @BindsOptionalOf
     @Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN)
     abstract BcSmartspaceDataPlugin optionalWeatherSmartspaceConfigPlugin();
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 7ff27cf..cd911c1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -347,7 +347,7 @@
         unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
 
     // TODO(b/263512203): Tracking Bug
-    val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true)
+    val MEDIA_EXPLICIT_INDICATOR = releasedFlag(911, "media_explicit_indicator")
 
     // TODO(b/265813373): Tracking Bug
     val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE =
@@ -360,6 +360,9 @@
     @JvmField
     val MEDIA_RECOMMENDATION_CARD_UPDATE = unreleasedFlag(914, "media_recommendation_card_update")
 
+    // TODO(b/267007629): Tracking Bug
+    val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress")
+
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
 
@@ -569,9 +572,8 @@
 
     // 2200 - udfps
     // TODO(b/259264861): Tracking Bug
-    @JvmField val UDFPS_NEW_TOUCH_DETECTION = unreleasedFlag(2200, "udfps_new_touch_detection")
-    @JvmField val UDFPS_ELLIPSE_DEBUG_UI = unreleasedFlag(2201, "udfps_ellipse_debug")
-    @JvmField val UDFPS_ELLIPSE_DETECTION = unreleasedFlag(2202, "udfps_ellipse_detection")
+    @JvmField val UDFPS_NEW_TOUCH_DETECTION = releasedFlag(2200, "udfps_new_touch_detection")
+    @JvmField val UDFPS_ELLIPSE_DETECTION = releasedFlag(2201, "udfps_ellipse_detection")
 
     // 2300 - stylus
     @JvmField val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used")
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 ce61f2f..86f65dde 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
@@ -21,6 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 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
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake
@@ -44,6 +45,7 @@
 
     override fun start() {
         listenForDozingToLockscreen()
+        listenForDozingToGone()
     }
 
     private fun listenForDozingToLockscreen() {
@@ -68,6 +70,28 @@
         }
     }
 
+    private fun listenForDozingToGone() {
+        scope.launch {
+            keyguardInteractor.biometricUnlockState
+                .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+                .collect { (biometricUnlockState, lastStartedTransition) ->
+                    if (
+                        lastStartedTransition.to == KeyguardState.DOZING &&
+                            isWakeAndUnlock(biometricUnlockState)
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.DOZING,
+                                KeyguardState.GONE,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
     private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
         return ValueAnimator().apply {
             setInterpolator(Interpolators.LINEAR)
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 ad6dbea..53c80f6 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
@@ -19,7 +19,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.AnimationParams
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
@@ -31,9 +30,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import javax.inject.Inject
-import kotlin.math.max
-import kotlin.math.min
-import kotlin.time.Duration
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
@@ -104,38 +100,4 @@
     /* The last completed [KeyguardState] transition */
     val finishedKeyguardState: Flow<KeyguardState> =
         finishedKeyguardTransitionStep.map { step -> step.to }
-
-    /**
-     * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the
-     * range of [0, 1]. View animations should begin and end within a subset of this range. This
-     * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
-     */
-    fun transitionStepAnimation(
-        flow: Flow<TransitionStep>,
-        params: AnimationParams,
-        totalDuration: Duration,
-    ): Flow<Float> {
-        val start = (params.startTime / totalDuration).toFloat()
-        val chunks = (totalDuration / params.duration).toFloat()
-        var isRunning = false
-        return flow
-            .map { step ->
-                val value = (step.value - start) * chunks
-                if (step.transitionState == STARTED) {
-                    // When starting, make sure to always emit. If a transition is started from the
-                    // middle, it is possible this animation is being skipped but we need to inform
-                    // the ViewModels of the last update
-                    isRunning = true
-                    max(0f, min(1f, value))
-                } else if (isRunning && value >= 1f) {
-                    // Always send a final value of 1. Because of rounding, [value] may never be
-                    // exactly 1.
-                    isRunning = false
-                    1f
-                } else {
-                    value
-                }
-            }
-            .filter { value -> value >= 0f && value <= 1f }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
deleted file mode 100644
index 67733e9..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ /dev/null
@@ -1,25 +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.keyguard.shared.model
-
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-
-/** Animation parameters */
-data class AnimationParams(
-    val startTime: Duration = 0.milliseconds,
-    val duration: Duration,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
new file mode 100644
index 0000000..ca1e27c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.ui
+
+import android.view.animation.Interpolator
+import com.android.systemui.animation.Interpolators.LINEAR
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+/**
+ * For the given transition params, construct a flow using [createFlow] for the specified portion of
+ * the overall transition.
+ */
+class KeyguardTransitionAnimationFlow(
+    private val transitionDuration: Duration,
+    private val transitionFlow: Flow<TransitionStep>,
+) {
+    /**
+     * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted in
+     * the range of [0, 1]. View animations should begin and end within a subset of this range. This
+     * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
+     */
+    fun createFlow(
+        duration: Duration,
+        onStep: (Float) -> Float,
+        startTime: Duration = 0.milliseconds,
+        onCancel: (() -> Float)? = null,
+        onFinish: (() -> Float)? = null,
+        interpolator: Interpolator = LINEAR,
+    ): Flow<Float> {
+        if (!duration.isPositive()) {
+            throw IllegalArgumentException("duration must be a positive number: $duration")
+        }
+        if ((startTime + duration).compareTo(transitionDuration) > 0) {
+            throw IllegalArgumentException(
+                "startTime($startTime) + duration($duration) must be" +
+                    " <= transitionDuration($transitionDuration)"
+            )
+        }
+
+        val start = (startTime / transitionDuration).toFloat()
+        val chunks = (transitionDuration / duration).toFloat()
+        var isComplete = true
+
+        fun stepToValue(step: TransitionStep): Float? {
+            val value = (step.value - start) * chunks
+            return when (step.transitionState) {
+                // When starting, make sure to always emit. If a transition is started from the
+                // middle, it is possible this animation is being skipped but we need to inform
+                // the ViewModels of the last update
+                STARTED -> {
+                    isComplete = false
+                    max(0f, min(1f, value))
+                }
+                // Always send a final value of 1. Because of rounding, [value] may never be
+                // exactly 1.
+                RUNNING ->
+                    if (isComplete) {
+                        null
+                    } else if (value >= 1f) {
+                        isComplete = true
+                        1f
+                    } else if (value >= 0f) {
+                        value
+                    } else {
+                        null
+                    }
+                else -> null
+            }?.let { onStep(interpolator.getInterpolation(it)) }
+        }
+
+        return transitionFlow
+            .map { step ->
+                when (step.transitionState) {
+                    STARTED -> stepToValue(step)
+                    RUNNING -> stepToValue(step)
+                    CANCELED -> onCancel?.invoke()
+                    FINISHED -> onFinish?.invoke()
+                }
+            }
+            .filterNotNull()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 6627865..8d6545a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -21,15 +21,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
 
 /**
  * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -41,39 +36,46 @@
 constructor(
     private val interactor: KeyguardTransitionInteractor,
 ) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_LOCKSCREEN_DURATION,
+            transitionFlow = interactor.dreamingToLockscreenTransition,
+        )
 
     /** Dream overlay y-translation on exit */
     fun dreamOverlayTranslationY(translatePx: Int): Flow<Float> {
-        return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value ->
-            EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx
-        }
+        return transitionAnimation.createFlow(
+            duration = 600.milliseconds,
+            onStep = { it * translatePx },
+            interpolator = EMPHASIZED_ACCELERATE,
+        )
     }
     /** Dream overlay views alpha - fade out */
-    val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
+    val dreamOverlayAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 250.milliseconds,
+            onStep = { 1f - it },
+        )
 
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return merge(
-            flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
-                -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
-            },
-            // On end, reset the translation to 0
-            interactor.dreamingToLockscreenTransition
-                .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
-                .map { 0f }
+        return transitionAnimation.createFlow(
+            duration = TO_LOCKSCREEN_DURATION,
+            onStep = { value -> -translatePx + value * translatePx },
+            // Reset on cancel or finish
+            onFinish = { 0f },
+            onCancel = { 0f },
+            interpolator = EMPHASIZED_DECELERATE,
         )
     }
 
     /** Lockscreen views alpha */
-    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
-    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
-        return interactor.transitionStepAnimation(
-            interactor.dreamingToLockscreenTransition,
-            params,
-            totalDuration = TO_LOCKSCREEN_DURATION
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            startTime = 233.milliseconds,
+            duration = 250.milliseconds,
+            onStep = { it },
         )
-    }
 
     companion object {
         /* Length of time before ending the dream activity, in order to start unoccluding */
@@ -81,11 +83,5 @@
         @JvmField
         val LOCKSCREEN_ANIMATION_DURATION_MS =
             (TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
-
-        val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
-        val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
-        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
-        val LOCKSCREEN_ALPHA =
-            AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
index 5a47960..f16827d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -20,15 +20,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
 
 /** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */
 @SysUISingleton
@@ -38,32 +33,28 @@
     private val interactor: KeyguardTransitionInteractor,
 ) {
 
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_DREAMING_DURATION,
+            transitionFlow = interactor.goneToDreamingTransition,
+        )
+
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return merge(
-            flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
-                (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
-            },
-            // On end, reset the translation to 0
-            interactor.goneToDreamingTransition
-                .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
-                .map { 0f }
+        return transitionAnimation.createFlow(
+            duration = 500.milliseconds,
+            onStep = { it * translatePx },
+            // Reset on cancel or finish
+            onFinish = { 0f },
+            onCancel = { 0f },
+            interpolator = EMPHASIZED_ACCELERATE,
         )
     }
 
     /** Lockscreen views alpha */
-    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
-    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
-        return interactor.transitionStepAnimation(
-            interactor.goneToDreamingTransition,
-            params,
-            totalDuration = TO_DREAMING_DURATION
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 250.milliseconds,
+            onStep = { 1f - it },
         )
-    }
-
-    companion object {
-        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
-        val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
index e05adbd..bc9dc4f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -20,15 +20,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
 
 /**
  * Breaks down LOCKSCREEN->DREAMING transition into discrete steps for corresponding views to
@@ -40,35 +35,32 @@
 constructor(
     private val interactor: KeyguardTransitionInteractor,
 ) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_DREAMING_DURATION,
+            transitionFlow = interactor.lockscreenToDreamingTransition,
+        )
 
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return merge(
-            flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
-                (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
-            },
-            // On end, reset the translation to 0
-            interactor.lockscreenToDreamingTransition
-                .filter { it.transitionState == FINISHED || it.transitionState == CANCELED }
-                .map { 0f }
+        return transitionAnimation.createFlow(
+            duration = 500.milliseconds,
+            onStep = { it * translatePx },
+            // Reset on cancel or finish
+            onFinish = { 0f },
+            onCancel = { 0f },
+            interpolator = EMPHASIZED_ACCELERATE,
         )
     }
 
     /** Lockscreen views alpha */
-    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
-    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
-        return interactor.transitionStepAnimation(
-            interactor.lockscreenToDreamingTransition,
-            params,
-            totalDuration = TO_DREAMING_DURATION
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 250.milliseconds,
+            onStep = { 1f - it },
         )
-    }
 
     companion object {
         @JvmField val DREAMING_ANIMATION_DURATION_MS = TO_DREAMING_DURATION.inWholeMilliseconds
-
-        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
-        val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index 22d292e..a60665a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -20,14 +20,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
-import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
 
 /**
  * Breaks down LOCKSCREEN->OCCLUDED transition into discrete steps for corresponding views to
@@ -39,33 +35,28 @@
 constructor(
     private val interactor: KeyguardTransitionInteractor,
 ) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_OCCLUDED_DURATION,
+            transitionFlow = interactor.lockscreenToOccludedTransition,
+        )
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            duration = 250.milliseconds,
+            onStep = { 1f - it },
+        )
 
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return merge(
-            flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
-                (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx)
-            },
-            // On end, reset the translation to 0
-            interactor.lockscreenToOccludedTransition
-                .filter { step -> step.transitionState == TransitionState.FINISHED }
-                .map { 0f }
+        return transitionAnimation.createFlow(
+            duration = TO_OCCLUDED_DURATION,
+            onStep = { value -> value * translatePx },
+            // Reset on cancel or finish
+            onFinish = { 0f },
+            onCancel = { 0f },
+            interpolator = EMPHASIZED_ACCELERATE,
         )
     }
-
-    /** Lockscreen views alpha */
-    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }
-
-    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
-        return interactor.transitionStepAnimation(
-            interactor.lockscreenToOccludedTransition,
-            params,
-            totalDuration = TO_OCCLUDED_DURATION
-        )
-    }
-
-    companion object {
-        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_OCCLUDED_DURATION)
-        val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index e804562..5770f3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -20,11 +20,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
 
 /**
  * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -36,28 +35,26 @@
 constructor(
     private val interactor: KeyguardTransitionInteractor,
 ) {
+    private val transitionAnimation =
+        KeyguardTransitionAnimationFlow(
+            transitionDuration = TO_LOCKSCREEN_DURATION,
+            transitionFlow = interactor.occludedToLockscreenTransition,
+        )
+
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
-            -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
-        }
-    }
-
-    /** Lockscreen views alpha */
-    val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
-    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
-        return interactor.transitionStepAnimation(
-            interactor.occludedToLockscreenTransition,
-            params,
-            totalDuration = TO_LOCKSCREEN_DURATION
+        return transitionAnimation.createFlow(
+            duration = TO_LOCKSCREEN_DURATION,
+            onStep = { value -> -translatePx + value * translatePx },
+            interpolator = EMPHASIZED_DECELERATE,
         )
     }
 
-    companion object {
-        @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
-        val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
-        val LOCKSCREEN_ALPHA =
-            AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
-    }
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.createFlow(
+            startTime = 233.milliseconds,
+            duration = 250.milliseconds,
+            onStep = { it },
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index be18cbe..b7a2522 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -92,6 +92,9 @@
 
     /** Whether explicit indicator exists */
     val isExplicit: Boolean = false,
+
+    /** Track progress (0 - 1) to display for players where [resumption] is true */
+    val resumeProgress: Double? = null,
 ) {
     companion object {
         /** Media is playing on the local device */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index bba5f35..a057c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -238,6 +238,24 @@
     }
 
     /**
+     * Set the progress to a fixed percentage value that cannot be changed by the user.
+     *
+     * @param percent value between 0 and 1
+     */
+    fun updateStaticProgress(percent: Double) {
+        val position = (percent * 100).toInt()
+        _data =
+            Progress(
+                enabled = true,
+                seekAvailable = false,
+                playing = false,
+                scrubbing = false,
+                elapsedTime = position,
+                duration = 100,
+            )
+    }
+
+    /**
      * Puts the seek bar into a resumption state.
      *
      * This should be called when the media session behind the controller has been destroyed.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index da2164e..16a2e3f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -68,6 +68,7 @@
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
 import com.android.systemui.media.controls.resume.MediaResumeListener
 import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.plugins.ActivityStarter
@@ -668,6 +669,11 @@
                 MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT &&
                 mediaFlags.isExplicitIndicatorEnabled()
 
+        val progress =
+            if (mediaFlags.isResumeProgressEnabled()) {
+                MediaDataUtils.getDescriptionProgress(desc.extras)
+            } else null
+
         val mediaAction = getResumeMediaAction(resumeAction)
         val lastActive = systemClock.elapsedRealtime()
         foregroundExecutor.execute {
@@ -698,6 +704,7 @@
                     instanceId = instanceId,
                     appUid = appUid,
                     isExplicit = isExplicit,
+                    resumeProgress = progress,
                 )
             )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 2fd4f27..61cc619 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -116,8 +116,6 @@
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.time.SystemClock;
 
-import dagger.Lazy;
-
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
@@ -125,6 +123,7 @@
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
 import kotlin.Triple;
 import kotlin.Unit;
 
@@ -524,8 +523,13 @@
         }
 
         // Seek Bar
-        final MediaController controller = getController();
-        mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+        if (data.getResumption() && data.getResumeProgress() != null) {
+            double progress = data.getResumeProgress();
+            mSeekBarViewModel.updateStaticProgress(progress);
+        } else {
+            final MediaController controller = getController();
+            mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+        }
 
         // Show the broadcast dialog button only when the le audio is enabled.
         mShowBroadcastDialogButton =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
index bcfceaa..85282a1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
@@ -19,8 +19,12 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.os.Bundle;
 import android.text.TextUtils;
 
+import androidx.core.math.MathUtils;
+import androidx.media.utils.MediaConstants;
+
 /**
  * Utility class with common methods for media controls
  */
@@ -50,4 +54,35 @@
                         : unknownName);
         return applicationName;
     }
+
+    /**
+     * Check the bundle for extras indicating the progress percentage
+     *
+     * @param extras
+     * @return the progress value between 0-1 inclusive if prsent, otherwise null
+     */
+    public static Double getDescriptionProgress(Bundle extras) {
+        if (!extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS)) {
+            return null;
+        }
+
+        int status = extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS);
+        switch (status) {
+            case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED:
+                return 0.0;
+            case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED:
+                return 1.0;
+            case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED: {
+                if (extras
+                        .containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE)) {
+                    double percent = extras
+                            .getDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE);
+                    return MathUtils.clamp(percent, 0.0, 1.0);
+                } else {
+                    return 0.5;
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 81efa36..a689dc3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -55,4 +55,7 @@
     /** Check whether we show the updated recommendation card. */
     fun isRecommendationCardUpdateEnabled() =
         featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)
+
+    /** Check whether to get progress information for resume players */
+    fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index d6c6a81..e57b169 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.media.dialog;
 
-import static android.media.RouteListingPreference.Item.DISABLE_REASON_SUBSCRIPTION_REQUIRED;
+import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
 
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -411,10 +411,10 @@
     @RequiresApi(34)
     private static class Api34Impl {
         @DoNotInline
-        static String composeDisabledReason(@RouteListingPreference.Item.DisableReason int reason,
-                Context context) {
+        static String composeDisabledReason(
+                @RouteListingPreference.Item.SubText int reason, Context context) {
             switch(reason) {
-                case DISABLE_REASON_SUBSCRIPTION_REQUIRED:
+                case SUBTEXT_SUBSCRIPTION_REQUIRED:
                     return context.getString(R.string.media_output_status_require_premium);
             }
             return "";
diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
index 1324d2c..c4a1ed4 100644
--- a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
@@ -19,7 +19,6 @@
 import android.view.WindowManagerGlobal
 import com.android.app.motiontool.DdmHandleMotionTool
 import com.android.app.motiontool.MotionToolManager
-import com.android.app.viewcapture.ViewCapture
 import com.android.systemui.CoreStartable
 import dagger.Binds
 import dagger.Module
@@ -38,17 +37,12 @@
         }
 
         @Provides
-        fun provideMotionToolManager(
-            viewCapture: ViewCapture,
-            windowManagerGlobal: WindowManagerGlobal
-        ): MotionToolManager {
-            return MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+        fun provideMotionToolManager(windowManagerGlobal: WindowManagerGlobal): MotionToolManager {
+            return MotionToolManager.getInstance(windowManagerGlobal)
         }
 
         @Provides
         fun provideWindowManagerGlobal(): WindowManagerGlobal = WindowManagerGlobal.getInstance()
-
-        @Provides fun provideViewCapture(): ViewCapture = ViewCapture.getInstance()
     }
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
index 66b7842..1b728b8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
@@ -67,10 +67,7 @@
             }
 
             // If label wasn't loaded, use a default
-            val badgedLabel =
-                packageManager.getUserBadgedLabel(label ?: defaultFileAppName(), userHandle)
-
-            return WorkProfileFirstRunData(badgedLabel, badgedIcon)
+            return WorkProfileFirstRunData(label ?: defaultFileAppName(), badgedIcon)
         }
         return null
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 479510a..2ac7f7a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -3151,17 +3151,11 @@
         }
         // The padding on this area is large enough that we can use a cheaper clipping strategy
         mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null);
-        if (!qsVisible && mSplitShadeEnabled) {
-            // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to
-            // be visible, otherwise you can see the bounds once swiping up to see bouncer
-            mScrimController.setNotificationsBounds(0, 0, 0, 0);
-        } else {
-            // Increase the height of the notifications scrim when not in split shade
-            // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
-            // in this case they are rendered off-screen
-            final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
-            mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
-        }
+        // Increase the height of the notifications scrim when not in split shade
+        // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
+        // in this case they are rendered off-screen
+        final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
+        mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
 
         if (mSplitShadeEnabled) {
             mKeyguardStatusBarViewController.setNoTopClipping();
@@ -3222,6 +3216,12 @@
     private int calculateQsBottomPosition(float qsExpansionFraction) {
         if (mTransitioningToFullShadeProgress > 0.0f) {
             return mTransitionToFullShadeQSPosition;
+        } else if (mSplitShadeEnabled) {
+            // in split shade - outside lockscreen transition handled above - we simply jump between
+            // two qs expansion values - either shade is closed and qs expansion is 0 or shade is
+            // open and qs expansion is 1
+            int qsBottomTarget = mQs.getDesiredHeight() + mLargeScreenShadeHeaderHeight;
+            return qsExpansionFraction > 0 ? qsBottomTarget : 0;
         } else {
             int qsBottomYFrom = (int) getHeaderTranslation() + mQs.getQsMinExpansionHeight();
             int expandedTopMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight : 0;
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
index 393279b..641131e 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
@@ -44,6 +44,11 @@
         const val DREAM_SMARTSPACE_PRECONDITION = "dream_smartspace_precondition"
 
         /**
+         * The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd).
+         */
+        const val DATE_SMARTSPACE_DATA_PLUGIN = "date_smartspace_data_plugin"
+
+        /**
          * The BcSmartspaceDataPlugin for the standalone weather.
          */
         const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 5b62d30..4d7496d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -52,6 +52,7 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.regionsampling.RegionSampler
 import com.android.systemui.shared.regionsampling.UpdateColorCallback
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DATE_SMARTSPACE_DATA_PLUGIN
 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.WEATHER_SMARTSPACE_DATA_PLUGIN
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -84,6 +85,8 @@
         @Main private val uiExecutor: Executor,
         @Background private val bgExecutor: Executor,
         @Main private val handler: Handler,
+        @Named(DATE_SMARTSPACE_DATA_PLUGIN)
+        optionalDatePlugin: Optional<BcSmartspaceDataPlugin>,
         @Named(WEATHER_SMARTSPACE_DATA_PLUGIN)
         optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
         optionalPlugin: Optional<BcSmartspaceDataPlugin>,
@@ -94,6 +97,7 @@
     }
 
     private var session: SmartspaceSession? = null
+    private val datePlugin: BcSmartspaceDataPlugin? = optionalDatePlugin.orElse(null)
     private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
     private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
     private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null)
@@ -223,7 +227,7 @@
         execution.assertIsMainThread()
 
         return featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED) &&
-                weatherPlugin != null
+                datePlugin != null && weatherPlugin != null
     }
 
     private fun updateBypassEnabled() {
@@ -232,6 +236,25 @@
     }
 
     /**
+     * Constructs the date view and connects it to the smartspace service.
+     */
+    fun buildAndConnectDateView(parent: ViewGroup): View? {
+        execution.assertIsMainThread()
+
+        if (!isEnabled()) {
+            throw RuntimeException("Cannot build view when not enabled")
+        }
+        if (!isDateWeatherDecoupled()) {
+            throw RuntimeException("Cannot build date view when not decoupled")
+        }
+
+        val view = buildView(parent, datePlugin)
+        connectSession()
+
+        return view
+    }
+
+    /**
      * Constructs the weather view and connects it to the smartspace service.
      */
     fun buildAndConnectWeatherView(parent: ViewGroup): View? {
@@ -309,7 +332,7 @@
     }
 
     private fun connectSession() {
-        if (weatherPlugin == null && plugin == null) return
+        if (datePlugin == null && weatherPlugin == null && plugin == null) return
         if (session != null || smartspaceViews.isEmpty()) {
             return
         }
@@ -347,6 +370,7 @@
         statusBarStateController.addCallback(statusBarStateListener)
         bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
 
+        datePlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
         weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
         plugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
 
@@ -384,6 +408,8 @@
         bypassController.unregisterOnBypassStateChangedListener(bypassStateChangedListener)
         session = null
 
+        datePlugin?.registerSmartspaceEventNotifier(null)
+
         weatherPlugin?.registerSmartspaceEventNotifier(null)
         weatherPlugin?.onTargetsAvailable(emptyList())
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 344d233..c1c6c88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -23,6 +23,7 @@
 import android.view.View;
 import android.view.ViewStub;
 
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.LockIconView;
 import com.android.systemui.R;
 import com.android.systemui.battery.BatteryMeterView;
@@ -67,6 +68,7 @@
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.settings.SecureSettings;
@@ -299,7 +301,9 @@
             OperatorNameViewController.Factory operatorNameViewControllerFactory,
             SecureSettings secureSettings,
             @Main Executor mainExecutor,
-            DumpManager dumpManager
+            DumpManager dumpManager,
+            StatusBarWindowStateController statusBarWindowStateController,
+            KeyguardUpdateMonitor keyguardUpdateMonitor
     ) {
         return new CollapsedStatusBarFragment(statusBarFragmentComponentFactory,
                 ongoingCallController,
@@ -320,7 +324,9 @@
                 operatorNameViewControllerFactory,
                 secureSettings,
                 mainExecutor,
-                dumpManager);
+                dumpManager,
+                statusBarWindowStateController,
+                keyguardUpdateMonitor);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index cbd27cf..65becf7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -44,6 +44,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
@@ -72,6 +73,8 @@
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.CarrierConfigTracker.CarrierConfigChangedListener;
 import com.android.systemui.util.CarrierConfigTracker.DefaultDataSubscriptionChangedListener;
@@ -129,6 +132,8 @@
     private final SecureSettings mSecureSettings;
     private final Executor mMainExecutor;
     private final DumpManager mDumpManager;
+    private final StatusBarWindowStateController mStatusBarWindowStateController;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
 
     private List<String> mBlockedIcons = new ArrayList<>();
     private Map<Startable, Startable.State> mStartableStates = new ArrayMap<>();
@@ -164,6 +169,22 @@
                 }
             };
 
+    /**
+     * Whether we've launched the secure camera over the lockscreen, but haven't yet received a
+     * status bar window state change afterward.
+     *
+     * We wait for this state change (which will tell us whether to show/hide the status bar icons)
+     * so that there is no flickering/jump cutting during the camera launch.
+     */
+    private boolean mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
+    /**
+     * Listener that updates {@link #mWaitingForWindowStateChangeAfterCameraLaunch} when it receives
+     * a new status bar window state.
+     */
+    private final StatusBarWindowStateListener mStatusBarWindowStateListener = state ->
+            mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
     @SuppressLint("ValidFragment")
     public CollapsedStatusBarFragment(
             StatusBarFragmentComponent.Factory statusBarFragmentComponentFactory,
@@ -185,7 +206,9 @@
             OperatorNameViewController.Factory operatorNameViewControllerFactory,
             SecureSettings secureSettings,
             @Main Executor mainExecutor,
-            DumpManager dumpManager
+            DumpManager dumpManager,
+            StatusBarWindowStateController statusBarWindowStateController,
+            KeyguardUpdateMonitor keyguardUpdateMonitor
     ) {
         mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
         mOngoingCallController = ongoingCallController;
@@ -207,6 +230,20 @@
         mSecureSettings = secureSettings;
         mMainExecutor = mainExecutor;
         mDumpManager = dumpManager;
+        mStatusBarWindowStateController = statusBarWindowStateController;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener);
     }
 
     @Override
@@ -254,6 +291,11 @@
         mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
     }
 
+    @Override
+    public void onCameraLaunchGestureDetected(int source) {
+        mWaitingForWindowStateChangeAfterCameraLaunch = true;
+    }
+
     @VisibleForTesting
     void updateBlockedIcons() {
         mBlockedIcons.clear();
@@ -466,6 +508,27 @@
                 && mNotificationPanelViewController.hideStatusBarIconsWhenExpanded()) {
             return true;
         }
+
+        // When launching the camera over the lockscreen, the icons become visible momentarily
+        // before animating out, since we're not yet aware that the launching camera activity is
+        // fullscreen. Even once the activity finishes launching, it takes a short time before WM
+        // decides that the top app wants to hide the icons and tells us to hide them. To ensure
+        // that this high-visibility animation is smooth, keep the icons hidden during a camera
+        // launch until we receive a window state change which indicates that the activity is done
+        // launching and WM has decided to show/hide the icons. For extra safety (to ensure the
+        // icons don't remain hidden somehow) we double check that the camera is still showing, the
+        // status bar window isn't hidden, and we're still occluded as well, though these checks
+        // are typically unnecessary.
+        final boolean hideIconsForSecureCamera =
+                (mWaitingForWindowStateChangeAfterCameraLaunch ||
+                        !mStatusBarWindowStateController.windowIsShowing()) &&
+                        mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard() &&
+                        mKeyguardStateController.isOccluded();
+
+        if (hideIconsForSecureCamera) {
+            return true;
+        }
+
         return mStatusBarHideIconsForBouncerManager.getShouldHideStatusBarIconsForBouncer();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
index 60f6df6..8f424b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
@@ -61,6 +61,10 @@
         listeners.add(listener)
     }
 
+    fun removeListener(listener: StatusBarWindowStateListener) {
+        listeners.remove(listener)
+    }
+
     /** Returns true if the window is currently showing. */
     fun windowIsShowing() = windowState == WINDOW_STATE_SHOWING
 
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 9f6e602..307787b 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -356,13 +356,22 @@
     };
 
     @Inject
-    public ThemeOverlayController(Context context, BroadcastDispatcher broadcastDispatcher,
-            @Background Handler bgHandler, @Main Executor mainExecutor,
-            @Background Executor bgExecutor, ThemeOverlayApplier themeOverlayApplier,
-            SecureSettings secureSettings, WallpaperManager wallpaperManager,
-            UserManager userManager, DeviceProvisionedController deviceProvisionedController,
-            UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
-            @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
+    public ThemeOverlayController(
+            Context context,
+            BroadcastDispatcher broadcastDispatcher,
+            @Background Handler bgHandler,
+            @Main Executor mainExecutor,
+            @Background Executor bgExecutor,
+            ThemeOverlayApplier themeOverlayApplier,
+            SecureSettings secureSettings,
+            WallpaperManager wallpaperManager,
+            UserManager userManager,
+            DeviceProvisionedController deviceProvisionedController,
+            UserTracker userTracker,
+            DumpManager dumpManager,
+            FeatureFlags featureFlags,
+            @Main Resources resources,
+            WakefulnessLifecycle wakefulnessLifecycle) {
         mContext = context;
         mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
         mDeviceProvisionedController = deviceProvisionedController;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index a4180fd..512c351 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -33,6 +33,7 @@
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
@@ -118,6 +119,11 @@
     @Mock
     private LogBuffer mLogBuffer;
 
+    private final View mFakeDateView = (View) (new ViewGroup(mContext) {
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {}
+    });
+    private final View mFakeWeatherView = new View(mContext);
     private final View mFakeSmartspaceView = new View(mContext);
 
     private KeyguardClockSwitchController mController;
@@ -145,6 +151,8 @@
         when(mLargeClockView.getContext()).thenReturn(getContext());
 
         when(mView.isAttachedToWindow()).thenReturn(true);
+        when(mSmartspaceController.buildAndConnectDateView(any())).thenReturn(mFakeDateView);
+        when(mSmartspaceController.buildAndConnectWeatherView(any())).thenReturn(mFakeWeatherView);
         when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
         mExecutor = new FakeExecutor(new FakeSystemClock());
         mController = new KeyguardClockSwitchController(
@@ -252,6 +260,19 @@
     }
 
     @Test
+    public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() {
+        when(mSmartspaceController.isEnabled()).thenReturn(true);
+        when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
+        mController.init();
+
+        mController.onLocaleListChanged();
+        // Should be called once on initial setup, then once again for locale change
+        verify(mSmartspaceController, times(2)).buildAndConnectDateView(mView);
+        verify(mSmartspaceController, times(2)).buildAndConnectWeatherView(mView);
+        verify(mSmartspaceController, times(2)).buildAndConnectView(mView);
+    }
+
+    @Test
     public void testSmartspaceDisabledShowsKeyguardStatusArea() {
         when(mSmartspaceController.isEnabled()).thenReturn(false);
         mController.init();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
index af46d9b..4b41537 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
@@ -16,30 +16,23 @@
 
 package com.android.systemui.biometrics.udfps
 
-import android.graphics.Point
 import android.graphics.Rect
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.EllipseOverlapDetectorParams
 import com.google.common.truth.Truth.assertThat
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 import org.junit.runners.Parameterized.Parameters
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.`when` as whenEver
 
 @SmallTest
 @RunWith(Parameterized::class)
 class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() {
-    val underTest = spy(EllipseOverlapDetector(neededPoints = 1))
-
-    @Before
-    fun setUp() {
-        // Use one single center point for testing, required or total number of points may change
-        whenEver(underTest.calculateSensorPoints(SENSOR))
-            .thenReturn(listOf(Point(SENSOR.centerX(), SENSOR.centerY())))
-    }
+    val underTest =
+        EllipseOverlapDetector(
+            EllipseOverlapDetectorParams(minOverlap = .4f, targetSize = .2f, stepSize = 1)
+        )
 
     @Test
     fun isGoodOverlap() {
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 3a871b4..702f3763 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
@@ -24,6 +24,7 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -437,6 +438,43 @@
         }
 
     @Test
+    fun `DOZING to GONE`() =
+        testScope.runTest {
+            // GIVEN a prior transition has run to DOZING
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.DOZING,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN biometrics succeeds with wake and unlock mode
+            keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+            runCurrent()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture())
+                }
+            // THEN a transition to DOZING should occur
+            assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.DOZING)
+            assertThat(info.to).isEqualTo(KeyguardState.GONE)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
     fun `GONE to DOZING`() =
         testScope.runTest {
             // GIVEN a device with AOD not available
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
new file mode 100644
index 0000000..a5b78b74
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+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.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionAnimationFlowTest : SysuiTestCase() {
+    private lateinit var underTest: KeyguardTransitionAnimationFlow
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        underTest =
+            KeyguardTransitionAnimationFlow(
+                1000.milliseconds,
+                repository.transitions,
+            )
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun zeroDurationThrowsException() = runTest {
+        val flow = underTest.createFlow(duration = 0.milliseconds, onStep = { it })
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun startTimePlusDurationGreaterThanTransitionDurationThrowsException() = runTest {
+        val flow =
+            underTest.createFlow(
+                startTime = 300.milliseconds,
+                duration = 800.milliseconds,
+                onStep = { it }
+            )
+    }
+
+    @Test
+    fun onFinishRunsWhenSpecified() = runTest {
+        val flow =
+            underTest.createFlow(
+                duration = 100.milliseconds,
+                onStep = { it },
+                onFinish = { 10f },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+        assertThat(animationValues()).isEqualTo(10f)
+    }
+
+    @Test
+    fun onCancelRunsWhenSpecified() = runTest {
+        val flow =
+            underTest.createFlow(
+                duration = 100.milliseconds,
+                onStep = { it },
+                onCancel = { 100f },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
+        assertThat(animationValues()).isEqualTo(100f)
+    }
+
+    @Test
+    fun usesStartTime() = runTest {
+        val flow =
+            underTest.createFlow(
+                startTime = 500.milliseconds,
+                duration = 500.milliseconds,
+                onStep = { it },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertThat(animationValues()).isEqualTo(0f)
+
+        // Should not emit a value
+        repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING))
+
+        repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 0f)
+        repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 0.2f)
+        repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 0.6f)
+        repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 1f)
+    }
+
+    @Test
+    fun usesInterpolator() = runTest {
+        val flow =
+            underTest.createFlow(
+                duration = 1000.milliseconds,
+                interpolator = EMPHASIZED_ACCELERATE,
+                onStep = { it },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0f))
+        repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.5f))
+        repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.6f))
+        repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.8f))
+        repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+        assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(1f))
+    }
+
+    @Test
+    fun usesOnStepToDoubleValue() = runTest {
+        val flow =
+            underTest.createFlow(
+                duration = 1000.milliseconds,
+                onStep = { it * 2 },
+            )
+        var animationValues = collectLastValue(flow)
+        repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+        assertFloat(animationValues(), 0f)
+        repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 0.6f)
+        repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 1.2f)
+        repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 1.6f)
+        repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+        assertFloat(animationValues(), 2f)
+    }
+
+    private fun assertFloat(actual: Float?, expected: Float) {
+        assertThat(actual!!).isWithin(0.01f).of(expected)
+    }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.GONE,
+            to = KeyguardState.DREAMING,
+            value = value,
+            transitionState = state,
+            ownerName = "GoneToDreamingTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 5571663..06e397d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -18,19 +18,13 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
 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.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -46,6 +40,7 @@
 class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: DreamingToLockscreenTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
+    private lateinit var transitionAnimation: KeyguardTransitionAnimationFlow
 
     @Before
     fun setUp() {
@@ -63,32 +58,18 @@
             val job =
                 underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(0.6f))
+            // ...up to here
+            repository.sendTransitionStep(step(0.8f))
             repository.sendTransitionStep(step(1f))
 
-            // Only 3 values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
-            assertThat(values.size).isEqualTo(3)
-            assertThat(values[0])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0f, DREAM_OVERLAY_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
-                    ) * pixels
-                )
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
 
             job.cancel()
         }
@@ -100,16 +81,18 @@
 
             val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this)
 
+            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.1f))
             repository.sendTransitionStep(step(0.5f))
+            // ...up to here
             repository.sendTransitionStep(step(1f))
 
             // Only two values should be present, since the dream overlay runs for a small fraction
             // of the overall animation time
-            assertThat(values.size).isEqualTo(2)
-            assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
-            assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
+            assertThat(values.size).isEqualTo(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -121,19 +104,15 @@
 
             val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
 
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.1f))
-            // Should start running here...
             repository.sendTransitionStep(step(0.2f))
             repository.sendTransitionStep(step(0.3f))
-            // ...up to here
             repository.sendTransitionStep(step(1f))
 
-            // Only two values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
-            assertThat(values.size).isEqualTo(2)
-            assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
-            assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+            assertThat(values.size).isEqualTo(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -147,58 +126,27 @@
             val job =
                 underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
             repository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(4)
-            assertThat(values[0])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[3])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(1f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
 
             job.cancel()
         }
 
-    private fun animValue(stepValue: Float, params: AnimationParams): Float {
-        val totalDuration = TO_LOCKSCREEN_DURATION
-        val startValue = (params.startTime / totalDuration).toFloat()
-
-        val multiplier = (totalDuration / params.duration).toFloat()
-        return (stepValue - startValue) * multiplier
-    }
-
-    private fun step(value: Float): TransitionStep {
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
         return TransitionStep(
             from = KeyguardState.DREAMING,
             to = KeyguardState.LOCKSCREEN,
             value = value,
-            transitionState = TransitionState.RUNNING,
+            transitionState = state,
             ownerName = "DreamingToLockscreenTransitionViewModelTest"
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 7fa204b..14c3b50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
 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.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -59,20 +55,18 @@
             val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
 
             // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.1f))
             repository.sendTransitionStep(step(0.2f))
-            // ...up to here
             repository.sendTransitionStep(step(0.3f))
+            // ...up to here
             repository.sendTransitionStep(step(1f))
 
             // Only three values should be present, since the dream overlay runs for a small
-            // fraction
-            // of the overall animation time
-            assertThat(values.size).isEqualTo(3)
-            assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
-            assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
-            assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA))
+            // fraction of the overall animation time
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -87,45 +81,19 @@
                 underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
             // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
-            // ...up to here
-            repository.sendTransitionStep(step(1f))
             // And a final reset event on CANCEL
             repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED))
 
-            assertThat(values.size).isEqualTo(4)
-            assertThat(values[0])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[3]).isEqualTo(0f)
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
             job.cancel()
         }
 
-    private fun animValue(stepValue: Float, params: AnimationParams): Float {
-        val totalDuration = TO_DREAMING_DURATION
-        val startValue = (params.startTime / totalDuration).toFloat()
-
-        val multiplier = (totalDuration / params.duration).toFloat()
-        return (stepValue - startValue) * multiplier
-    }
-
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 539fc2c..ed31dc3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
 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.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -59,19 +55,18 @@
             val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
 
             // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.1f))
             repository.sendTransitionStep(step(0.2f))
-            // ...up to here
             repository.sendTransitionStep(step(0.3f))
+            // ...up to here
             repository.sendTransitionStep(step(1f))
 
             // Only three values should be present, since the dream overlay runs for a small
             // fraction of the overall animation time
-            assertThat(values.size).isEqualTo(3)
-            assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
-            assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
-            assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA))
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -85,47 +80,22 @@
             val job =
                 underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
-            // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
-            // ...up to here
             repository.sendTransitionStep(step(1f))
             // And a final reset event on FINISHED
             repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
 
-            assertThat(values.size).isEqualTo(4)
-            assertThat(values[0])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[3]).isEqualTo(0f)
+            assertThat(values.size).isEqualTo(6)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+            // Validate finished value
+            assertThat(values[5]).isEqualTo(0f)
 
             job.cancel()
         }
 
-    private fun animValue(stepValue: Float, params: AnimationParams): Float {
-        val totalDuration = TO_DREAMING_DURATION
-        val startValue = (params.startTime / totalDuration).toFloat()
-
-        val multiplier = (totalDuration / params.duration).toFloat()
-        return (stepValue - startValue) * multiplier
-    }
-
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 759345f..458b315 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
 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.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -59,19 +55,18 @@
             val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
 
             // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.1f))
             repository.sendTransitionStep(step(0.4f))
-            // ...up to here
             repository.sendTransitionStep(step(0.7f))
+            // ...up to here
             repository.sendTransitionStep(step(1f))
 
             // Only 3 values should be present, since the dream overlay runs for a small fraction
             // of the overall animation time
-            assertThat(values.size).isEqualTo(3)
-            assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA))
-            assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA))
-            assertThat(values[2]).isEqualTo(1f - animValue(0.4f, LOCKSCREEN_ALPHA))
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -86,54 +81,51 @@
                 underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
             // Should start running here...
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
             repository.sendTransitionStep(step(1f))
             // ...up to here
 
-            assertThat(values.size).isEqualTo(4)
-            assertThat(values[0])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
-            assertThat(values[3])
-                .isEqualTo(
-                    EMPHASIZED_ACCELERATE.getInterpolation(
-                        animValue(1f, LOCKSCREEN_TRANSLATION_Y)
-                    ) * pixels
-                )
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
             job.cancel()
         }
 
-    private fun animValue(stepValue: Float, params: AnimationParams): Float {
-        val totalDuration = TO_OCCLUDED_DURATION
-        val startValue = (params.startTime / totalDuration).toFloat()
+    @Test
+    fun lockscreenTranslationYIsCanceled() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
 
-        val multiplier = (totalDuration / params.duration).toFloat()
-        return (stepValue - startValue) * multiplier
-    }
+            val pixels = 100
+            val job =
+                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
-    private fun step(value: Float): TransitionStep {
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.3f, TransitionState.CANCELED))
+
+            assertThat(values.size).isEqualTo(4)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+            // Cancel will reset the translation
+            assertThat(values[3]).isEqualTo(0)
+
+            job.cancel()
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING,
+    ): TransitionStep {
         return TransitionStep(
             from = KeyguardState.LOCKSCREEN,
             to = KeyguardState.OCCLUDED,
             value = value,
-            transitionState = TransitionState.RUNNING,
+            transitionState = state,
             ownerName = "LockscreenToOccludedTransitionViewModelTest"
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index 98d292d..a36214e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
 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.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -58,21 +54,19 @@
 
             val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
 
-            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0.1f))
             // Should start running here...
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.4f))
             repository.sendTransitionStep(step(0.5f))
-            // ...up to here
             repository.sendTransitionStep(step(0.6f))
+            // ...up to here
+            repository.sendTransitionStep(step(0.8f))
             repository.sendTransitionStep(step(1f))
 
-            // Only two values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
-            assertThat(values.size).isEqualTo(3)
-            assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
-            assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA))
-            assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA))
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
 
             job.cancel()
         }
@@ -86,58 +80,27 @@
             val job =
                 underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
 
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
             repository.sendTransitionStep(step(0f))
             repository.sendTransitionStep(step(0.3f))
             repository.sendTransitionStep(step(0.5f))
             repository.sendTransitionStep(step(1f))
 
-            assertThat(values.size).isEqualTo(4)
-            assertThat(values[0])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[1])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[2])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
-            assertThat(values[3])
-                .isEqualTo(
-                    -pixels +
-                        EMPHASIZED_DECELERATE.getInterpolation(
-                            animValue(1f, LOCKSCREEN_TRANSLATION_Y)
-                        ) * pixels
-                )
+            assertThat(values.size).isEqualTo(5)
+            values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
 
             job.cancel()
         }
 
-    private fun animValue(stepValue: Float, params: AnimationParams): Float {
-        val totalDuration = TO_LOCKSCREEN_DURATION
-        val startValue = (params.startTime / totalDuration).toFloat()
-
-        val multiplier = (totalDuration / params.duration).toFloat()
-        return (stepValue - startValue) * multiplier
-    }
-
-    private fun step(value: Float): TransitionStep {
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
         return TransitionStep(
             from = KeyguardState.OCCLUDED,
             to = KeyguardState.LOCKSCREEN,
             value = value,
-            transitionState = TransitionState.RUNNING,
+            transitionState = state,
             ownerName = "OccludedToLockscreenTransitionViewModelTest"
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index c0639f3..0a5b124 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -79,7 +79,7 @@
                 USER_ID, true, APP, null, ARTIST, TITLE, null,
                 new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
                 MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
-                InstanceId.fakeInstanceId(-1), -1, false);
+                InstanceId.fakeInstanceId(-1), -1, false, null);
         mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index 1ac6695..53cc78f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -644,27 +644,8 @@
                 build()
             }
         val currentTime = clock.elapsedRealtime()
-        mediaDataManager.addResumptionControls(
-            USER_ID,
-            desc,
-            Runnable {},
-            session.sessionToken,
-            APP_NAME,
-            pendingIntent,
-            PACKAGE_NAME
-        )
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        // THEN the media data indicates that it is for resumption
-        verify(listener)
-            .onMediaDataLoaded(
-                eq(PACKAGE_NAME),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
+        addResumeControlAndLoad(desc)
+
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -690,27 +671,8 @@
                 build()
             }
         val currentTime = clock.elapsedRealtime()
-        mediaDataManager.addResumptionControls(
-            USER_ID,
-            desc,
-            Runnable {},
-            session.sessionToken,
-            APP_NAME,
-            pendingIntent,
-            PACKAGE_NAME
-        )
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        // THEN the media data indicates that it is for resumption
-        verify(listener)
-            .onMediaDataLoaded(
-                eq(PACKAGE_NAME),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
+        addResumeControlAndLoad(desc)
+
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -723,6 +685,84 @@
     }
 
     @Test
+    fun testAddResumptionControls_hasPartialProgress() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added with partial progress
+        val progress = 0.5
+        val extras =
+            Bundle().apply {
+                putInt(
+                    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+                )
+                putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress)
+            }
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                setExtras(extras)
+                build()
+            }
+        addResumeControlAndLoad(desc)
+
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.resumeProgress).isEqualTo(progress)
+    }
+
+    @Test
+    fun testAddResumptionControls_hasNotPlayedProgress() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added that have not been played
+        val extras =
+            Bundle().apply {
+                putInt(
+                    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
+                )
+            }
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                setExtras(extras)
+                build()
+            }
+        addResumeControlAndLoad(desc)
+
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.resumeProgress).isEqualTo(0)
+    }
+
+    @Test
+    fun testAddResumptionControls_hasFullProgress() {
+        whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+        // WHEN resumption controls are added with progress info
+        val extras =
+            Bundle().apply {
+                putInt(
+                    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
+                )
+            }
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                setExtras(extras)
+                build()
+            }
+        addResumeControlAndLoad(desc)
+
+        // THEN the media data includes the progress
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.resumeProgress).isEqualTo(1)
+    }
+
+    @Test
     fun testResumptionDisabled_dismissesResumeControls() {
         // WHEN there are resume controls and resumption is switched off
         val desc =
@@ -730,26 +770,8 @@
                 setTitle(SESSION_TITLE)
                 build()
             }
-        mediaDataManager.addResumptionControls(
-            USER_ID,
-            desc,
-            Runnable {},
-            session.sessionToken,
-            APP_NAME,
-            pendingIntent,
-            PACKAGE_NAME
-        )
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener)
-            .onMediaDataLoaded(
-                eq(PACKAGE_NAME),
-                eq(null),
-                capture(mediaDataCaptor),
-                eq(true),
-                eq(0),
-                eq(false)
-            )
+        addResumeControlAndLoad(desc)
+
         val data = mediaDataCaptor.value
         mediaDataManager.setMediaResumptionEnabled(false)
 
@@ -1690,4 +1712,29 @@
         stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f)
         whenever(controller.playbackState).thenReturn(stateBuilder.build())
     }
+
+    /** Helper function to add a resumption control and capture the resulting MediaData */
+    private fun addResumeControlAndLoad(desc: MediaDescription) {
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index ce22b19..26b9204 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -906,6 +906,17 @@
     }
 
     @Test
+    fun bind_resumeState_withProgress() {
+        val progress = 0.5
+        val state = mediaData.copy(resumption = true, resumeProgress = progress)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        verify(seekBarViewModel).updateStaticProgress(progress)
+    }
+
+    @Test
     fun bindNotificationActions() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 5564774..f40e3a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.media.dialog;
 
-import static android.media.RouteListingPreference.Item.DISABLE_REASON_SUBSCRIPTION_REQUIRED;
+import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQUIRED;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -436,7 +436,7 @@
                 R.string.media_output_status_require_premium);
         when(mMediaOutputController.isSubStatusSupported()).thenReturn(true);
         when(mMediaDevice2.hasDisabledReason()).thenReturn(true);
-        when(mMediaDevice2.getDisableReason()).thenReturn(DISABLE_REASON_SUBSCRIPTION_REQUIRED);
+        when(mMediaDevice2.getDisableReason()).thenReturn(SUBTEXT_SUBSCRIPTION_REQUIRED);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
         assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
index 576652f..3440f91 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
@@ -59,9 +59,7 @@
 @RunWith(AndroidTestingRunner.class)
 public class WorkProfileMessageControllerTest extends SysuiTestCase {
     private static final String DEFAULT_LABEL = "default label";
-    private static final String BADGED_DEFAULT_LABEL = "badged default label";
     private static final String APP_LABEL = "app label";
-    private static final String BADGED_APP_LABEL = "badged app label";
     private static final UserHandle NON_WORK_USER = UserHandle.of(0);
     private static final UserHandle WORK_USER = UserHandle.of(10);
 
@@ -91,10 +89,6 @@
                 eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME),
                 eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences);
         when(mMockContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL);
-        when(mPackageManager.getUserBadgedLabel(eq(DEFAULT_LABEL), any()))
-                .thenReturn(BADGED_DEFAULT_LABEL);
-        when(mPackageManager.getUserBadgedLabel(eq(APP_LABEL), any()))
-                .thenReturn(BADGED_APP_LABEL);
         when(mPackageManager.getActivityIcon(any(ComponentName.class)))
                 .thenReturn(mActivityIcon);
         when(mPackageManager.getUserBadgedIcon(
@@ -133,7 +127,7 @@
         WorkProfileMessageController.WorkProfileFirstRunData data =
                 mMessageController.onScreenshotTaken(WORK_USER);
 
-        assertEquals(BADGED_DEFAULT_LABEL, data.getAppName());
+        assertEquals(DEFAULT_LABEL, data.getAppName());
         assertNull(data.getIcon());
     }
 
@@ -142,7 +136,7 @@
         WorkProfileMessageController.WorkProfileFirstRunData data =
                 mMessageController.onScreenshotTaken(WORK_USER);
 
-        assertEquals(BADGED_APP_LABEL, data.getAppName());
+        assertEquals(APP_LABEL, data.getAppName());
         assertEquals(mBadgedActivityIcon, data.getIcon());
     }
 
@@ -151,7 +145,7 @@
         ViewGroup layout = (ViewGroup) LayoutInflater.from(mContext).inflate(
                 R.layout.screenshot_work_profile_first_run, null);
         WorkProfileMessageController.WorkProfileFirstRunData data =
-                new WorkProfileMessageController.WorkProfileFirstRunData(BADGED_APP_LABEL,
+                new WorkProfileMessageController.WorkProfileFirstRunData(APP_LABEL,
                         mBadgedActivityIcon);
         final CountDownLatch countdown = new CountDownLatch(1);
         mMessageController.populateView(layout, data, () -> {
@@ -163,7 +157,7 @@
         assertEquals(mBadgedActivityIcon, image.getDrawable());
         TextView text = layout.findViewById(R.id.screenshot_message_content);
         // The app name is used in a template, but at least validate that it was inserted.
-        assertTrue(text.getText().toString().contains(BADGED_APP_LABEL));
+        assertTrue(text.getText().toString().contains(APP_LABEL));
 
         // Validate that clicking the dismiss button calls back properly.
         assertEquals(1, countdown.getCount());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 2423f13..0a576de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -113,6 +113,9 @@
     private lateinit var handler: Handler
 
     @Mock
+    private lateinit var datePlugin: BcSmartspaceDataPlugin
+
+    @Mock
     private lateinit var weatherPlugin: BcSmartspaceDataPlugin
 
     @Mock
@@ -155,6 +158,7 @@
         KeyguardBypassController.OnBypassStateChangedListener
     private lateinit var deviceProvisionedListener: DeviceProvisionedListener
 
+    private lateinit var dateSmartspaceView: SmartspaceView
     private lateinit var weatherSmartspaceView: SmartspaceView
     private lateinit var smartspaceView: SmartspaceView
 
@@ -190,6 +194,8 @@
         `when`(secureSettings.getUriFor(NOTIF_ON_LOCKSCREEN_SETTING))
                 .thenReturn(fakeNotifOnLockscreenSettingUri)
         `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession)
+        `when`(datePlugin.getView(any())).thenReturn(
+                createDateSmartspaceView(), createDateSmartspaceView())
         `when`(weatherPlugin.getView(any())).thenReturn(
                 createWeatherSmartspaceView(), createWeatherSmartspaceView())
         `when`(plugin.getView(any())).thenReturn(createSmartspaceView(), createSmartspaceView())
@@ -221,6 +227,7 @@
                 executor,
                 bgExecutor,
                 handler,
+                Optional.of(datePlugin),
                 Optional.of(weatherPlugin),
                 Optional.of(plugin),
                 Optional.of(configPlugin),
@@ -275,7 +282,8 @@
 
         // THEN the listener is registered to the underlying plugin
         verify(plugin).registerListener(controllerListener)
-        // The listener is registered only for the plugin, not the weather plugin.
+        // The listener is registered only for the plugin, not the date, or weather plugin.
+        verify(datePlugin, never()).registerListener(any())
         verify(weatherPlugin, never()).registerListener(any())
     }
 
@@ -289,7 +297,8 @@
 
         // THEN the listener is subsequently registered
         verify(plugin).registerListener(controllerListener)
-        // The listener is registered only for the plugin, not the weather plugin.
+        // The listener is registered only for the plugin, not the date, or the weather plugin.
+        verify(datePlugin, never()).registerListener(any())
         verify(weatherPlugin, never()).registerListener(any())
     }
 
@@ -308,6 +317,7 @@
         verify(plugin).registerSmartspaceEventNotifier(null)
         verify(weatherPlugin).onTargetsAvailable(emptyList())
         verify(weatherPlugin).registerSmartspaceEventNotifier(null)
+        verify(datePlugin).registerSmartspaceEventNotifier(null)
     }
 
     @Test
@@ -357,6 +367,7 @@
         configChangeListener.onThemeChanged()
 
         // We update the new text color to match the wallpaper color
+        verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
         verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
         verify(smartspaceView).setPrimaryTextColor(anyInt())
     }
@@ -384,6 +395,7 @@
         statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
 
         // We pass that along to the view
+        verify(dateSmartspaceView).setDozeAmount(0.7f)
         verify(weatherSmartspaceView).setDozeAmount(0.7f)
         verify(smartspaceView).setDozeAmount(0.7f)
     }
@@ -502,6 +514,8 @@
         verify(plugin).onTargetsAvailable(eq(listOf(targets[0], targets[1], targets[2])))
         // No filtering is applied for the weather plugin
         verify(weatherPlugin).onTargetsAvailable(eq(targets))
+        // No targets needed for the date plugin
+        verify(datePlugin, never()).onTargetsAvailable(any())
     }
 
     @Test
@@ -633,6 +647,18 @@
 
     private fun connectSession() {
         if (controller.isDateWeatherDecoupled()) {
+            val dateView = controller.buildAndConnectDateView(fakeParent)
+            dateSmartspaceView = dateView as SmartspaceView
+            fakeParent.addView(dateView)
+            controller.stateChangeListener.onViewAttachedToWindow(dateView)
+
+            verify(dateSmartspaceView).setUiSurface(
+                    BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+            verify(dateSmartspaceView).registerDataProvider(datePlugin)
+
+            verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
+            verify(dateSmartspaceView).setDozeAmount(0.5f)
+
             val weatherView = controller.buildAndConnectWeatherView(fakeParent)
             weatherSmartspaceView = weatherView as SmartspaceView
             fakeParent.addView(weatherView)
@@ -686,6 +712,7 @@
         verify(smartspaceView).setDozeAmount(0.5f)
 
         if (controller.isDateWeatherDecoupled()) {
+            clearInvocations(dateSmartspaceView)
             clearInvocations(weatherSmartspaceView)
         }
         clearInvocations(smartspaceView)
@@ -734,7 +761,38 @@
         ).thenReturn(if (value) 1 else 0)
     }
 
-    // Separate function for the weather view, which doesn't implement all functions in interface.
+    // Separate function for the date view, which implements a specific subset of all functions.
+    private fun createDateSmartspaceView(): SmartspaceView {
+        return spy(object : View(context), SmartspaceView {
+            override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+            }
+
+            override fun setPrimaryTextColor(color: Int) {
+            }
+
+            override fun setIsDreaming(isDreaming: Boolean) {
+            }
+
+            override fun setUiSurface(uiSurface: String) {
+            }
+
+            override fun setDozeAmount(amount: Float) {
+            }
+
+            override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+            }
+
+            override fun setFalsingManager(falsingManager: FalsingManager?) {
+            }
+
+            override fun setDnd(image: Drawable?, description: String?) {
+            }
+
+            override fun setNextAlarm(image: Drawable?, description: String?) {
+            }
+        })
+    }
+    // Separate function for the weather view, which implements a specific subset of all functions.
     private fun createWeatherSmartspaceView(): SmartspaceView {
         return spy(object : View(context), SmartspaceView {
             override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 36e76f4..85e8c34 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -23,10 +23,12 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -45,6 +47,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.R;
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.dump.DumpManager;
@@ -67,6 +70,8 @@
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
@@ -79,6 +84,9 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
@@ -118,6 +126,12 @@
     private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock
     private DumpManager mDumpManager;
+    @Mock
+    private StatusBarWindowStateController mStatusBarWindowStateController;
+    @Mock
+    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+    private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
 
     public CollapsedStatusBarFragmentTest() {
         super(CollapsedStatusBarFragment.class);
@@ -127,6 +141,14 @@
     public void setup() {
         injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
         mDependency.injectMockDependency(DarkIconDispatcher.class);
+
+        // Keep the window state listeners so we can dispatch to them to test the status bar
+        // fragment's response.
+        doAnswer(invocation -> {
+            mStatusBarWindowStateListeners.add(invocation.getArgument(0));
+            return null;
+        }).when(mStatusBarWindowStateController).addListener(
+                any(StatusBarWindowStateListener.class));
     }
 
     @Test
@@ -414,6 +436,27 @@
         assertFalse(contains);
     }
 
+    @Test
+    public void testStatusBarIcons_hiddenThroughoutCameraLaunch() {
+        final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+
+        mockSecureCameraLaunch(fragment, true /* launched */);
+
+        // Status icons should be invisible or gone, but certainly not VISIBLE.
+        assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+        mockSecureCameraLaunchFinished();
+
+        assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+        mockSecureCameraLaunch(fragment, false /* launched */);
+
+        assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+        assertEquals(View.VISIBLE, getClockView().getVisibility());
+    }
+
     @Override
     protected Fragment instantiate(Context context, String className, Bundle arguments) {
         MockitoAnnotations.initMocks(this);
@@ -455,7 +498,9 @@
                 mOperatorNameViewControllerFactory,
                 mSecureSettings,
                 mExecutor,
-                mDumpManager);
+                mDumpManager,
+                mStatusBarWindowStateController,
+                mKeyguardUpdateMonitor);
     }
 
     private void setUpDaggerComponent() {
@@ -478,6 +523,35 @@
                 mNotificationAreaInner);
     }
 
+    /**
+     * Configure mocks to return values consistent with the secure camera animating itself launched
+     * over the keyguard.
+     */
+    private void mockSecureCameraLaunch(CollapsedStatusBarFragment fragment, boolean launched) {
+        when(mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard()).thenReturn(launched);
+        when(mKeyguardStateController.isOccluded()).thenReturn(launched);
+
+        if (launched) {
+            fragment.onCameraLaunchGestureDetected(0 /* source */);
+        } else {
+            for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+                listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_SHOWING);
+            }
+        }
+
+        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
+    }
+
+    /**
+     * Configure mocks to return values consistent with the secure camera showing over the keyguard
+     * with its launch animation finished.
+     */
+    private void mockSecureCameraLaunchFinished() {
+        for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+            listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_HIDDEN);
+        }
+    }
+
     private CollapsedStatusBarFragment resumeAndGetFragment() {
         mFragments.dispatchResume();
         processAllMessages();
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index f93f504..b6a2a0e 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -96,7 +96,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
 /**
@@ -287,7 +286,7 @@
          * - dynamically installed mobile bundled apps (MBAs) (new in Android U)
          */
         public void recordMeasurementsForAllPackages() {
-            // check if we should record the resulting measurements
+            // check if we should measure and record
             long currentTimeMs = System.currentTimeMillis();
             if ((currentTimeMs - mMeasurementsLastRecordedMs) < RECORD_MEASUREMENTS_COOLDOWN_MS) {
                 Slog.d(TAG, "Skip measurement since the last measurement was only taken at "
@@ -1230,10 +1229,8 @@
      * JobService to measure all covered binaries and record result to Westworld.
      */
     public static class UpdateMeasurementsJobService extends JobService {
-        private static AtomicBoolean sScheduled = new AtomicBoolean();
         private static long sTimeLastRanMs = 0;
-        private static final int DO_BINARY_MEASUREMENTS_JOB_ID =
-                UpdateMeasurementsJobService.class.hashCode();
+        private static final int DO_BINARY_MEASUREMENTS_JOB_ID = 1740526926;
 
         @Override
         public boolean onStartJob(JobParameters params) {
@@ -1256,7 +1253,6 @@
                     return;
                 }
                 sTimeLastRanMs = System.currentTimeMillis();
-                sScheduled.set(false);
                 jobFinished(params, false);
             }).start();
 
@@ -1277,7 +1273,7 @@
                 return;
             }
 
-            if (sScheduled.get()) {
+            if (jobScheduler.getPendingJob(DO_BINARY_MEASUREMENTS_JOB_ID) != null) {
                 Slog.d(TAG, "A measurement job has already been scheduled.");
                 return;
             }
@@ -1303,7 +1299,6 @@
                 Slog.e(TAG, "Failed to schedule job to measure binaries.");
                 return;
             }
-            sScheduled.set(true);
             Slog.d(TAG, TextUtils.formatSimple(
                     "Job %d to measure binaries was scheduled successfully.",
                     DO_BINARY_MEASUREMENTS_JOB_ID));
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 4854c37..4b76127 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -333,6 +333,11 @@
     // Update ownership for system applications and the installers eligible to update them.
     private final ArrayMap<String, String> mUpdateOwnersForSystemApps = new ArrayMap<>();
 
+    // Set of package names that should not be marked as "stopped" during initial device boot
+    // or when adding a new user. A new package not contained in this set will be
+    // marked as stopped by the system
+    @NonNull private final Set<String> mInitialNonStoppedSystemPackages = new ArraySet<>();
+
     /**
      * Map of system pre-defined, uniquely named actors; keys are namespace,
      * value maps actor name to package name.
@@ -527,6 +532,10 @@
                 ? null : mOverlayConfigSignaturePackage;
     }
 
+    public Set<String> getInitialNonStoppedSystemPackages() {
+        return mInitialNonStoppedSystemPackages;
+    }
+
     /**
      * Only use for testing. Do NOT use in production code.
      * @param readPermissions false to create an empty SystemConfig; true to read the permissions.
@@ -1445,6 +1454,19 @@
                         }
                         XmlUtils.skipCurrentTag(parser);
                     } break;
+                    case "initial-package-state": {
+                        String pkgName = parser.getAttributeValue(null, "package");
+                        String stopped = parser.getAttributeValue(null, "stopped");
+                        if (TextUtils.isEmpty(pkgName)) {
+                            Slog.w(TAG, "<" + name + "> without package in " + permFile
+                                    + " at " + parser.getPositionDescription());
+                        } else if (TextUtils.isEmpty(stopped)) {
+                            Slog.w(TAG, "<" + name + "> without stopped in " + permFile
+                                    + " at " + parser.getPositionDescription());
+                        } else if (!Boolean.parseBoolean(stopped)) {
+                            mInitialNonStoppedSystemPackages.add(pkgName);
+                        }
+                    }
                     default: {
                         Slog.w(TAG, "Tag " + name + " is unknown in "
                                 + permFile + " at " + parser.getPositionDescription());
diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING
index ae65dcb..cc8aec7 100644
--- a/services/core/java/com/android/server/TEST_MAPPING
+++ b/services/core/java/com/android/server/TEST_MAPPING
@@ -69,5 +69,11 @@
             ],
             "file_patterns": ["ClipboardService\\.java"]
         }
+    ],
+    "postsubmit": [
+        {
+            "name": "BinaryTransparencyHostTest",
+            "file_patterns": ["BinaryTransparencyService\\.java"]
+        }
     ]
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 57a89e3..423a090 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18974,14 +18974,20 @@
                 return null;
             }
 
-            // Starts with all displays but DEFAULT_DISPLAY
-            int[] displayIds = new int[allDisplays.length - 1];
+            boolean allowOnDefaultDisplay = UserManager
+                    .isVisibleBackgroundUsersOnDefaultDisplayEnabled();
+            int displaysSize = allDisplays.length;
+            if (!allowOnDefaultDisplay) {
+                displaysSize--;
+            }
+            int[] displayIds = new int[displaysSize];
 
-            // TODO(b/247592632): check for other properties like isSecure or proper display type
             int numberValidDisplays = 0;
             for (Display display : allDisplays) {
                 int displayId = display.getDisplayId();
-                if (display.isValid() && displayId != Display.DEFAULT_DISPLAY) {
+                // TODO(b/247592632): check other properties like isSecure or proper display type
+                if (display.isValid()
+                        && (allowOnDefaultDisplay || displayId != Display.DEFAULT_DISPLAY)) {
                     displayIds[numberValidDisplays++] = displayId;
                 }
             }
@@ -18993,14 +18999,15 @@
                 // STOPSHIP: if not removed, it should at least be unit tested
                 String testingProp = "fw.display_ids_for_starting_users_for_testing_purposes";
                 int displayId = SystemProperties.getInt(testingProp, Display.DEFAULT_DISPLAY);
-                if (displayId != Display.DEFAULT_DISPLAY && displayId > 0) {
-                    Slogf.w(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): no valid "
+                if (allowOnDefaultDisplay && displayId == Display.DEFAULT_DISPLAY
+                        || displayId > 0) {
+                    Slogf.w(TAG, "getDisplayIdsForStartingVisibleBackgroundUsers(): no valid "
                             + "display found, but returning %d as set by property %s", displayId,
                             testingProp);
                     return new int[] { displayId };
                 }
-                Slogf.e(TAG, "getDisplayIdsForStartingBackgroundUsers(): no valid display on %s",
-                        Arrays.toString(allDisplays));
+                Slogf.e(TAG, "getDisplayIdsForStartingVisibleBackgroundUsers(): no valid display"
+                        + " on %s", Arrays.toString(allDisplays));
                 return null;
             }
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index ed59a88..d888c81 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1462,7 +1462,7 @@
 
         mNm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
 
-        mSoundDoseHelper.configureSafeMediaVolume(/*forced=*/true, TAG);
+        mSoundDoseHelper.configureSafeMedia(/*forced=*/true, TAG);
 
         initA11yMonitoring();
 
@@ -10236,7 +10236,7 @@
             // reading new configuration "safely" (i.e. under try catch) in case anything
             // goes wrong.
             Configuration config = context.getResources().getConfiguration();
-            mSoundDoseHelper.configureSafeMediaVolume(/*forced*/false, TAG);
+            mSoundDoseHelper.configureSafeMedia(/*forced*/false, TAG);
 
             boolean cameraSoundForced = readCameraSoundForced();
             synchronized (mSettingsLock) {
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 42031c6..ac02eb7 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -38,6 +38,7 @@
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.MathUtils;
 
@@ -55,6 +56,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Safe media volume management.
@@ -77,14 +79,16 @@
     // SAFE_MEDIA_VOLUME_DISABLED according to country option. If not SAFE_MEDIA_VOLUME_DISABLED, it
     // can be set to SAFE_MEDIA_VOLUME_INACTIVE by calling AudioService.disableSafeMediaVolume()
     // (when user opts out).
+    // Note: when CSD calculation is enabled the state is set to SAFE_MEDIA_VOLUME_DISABLED
     private static final int SAFE_MEDIA_VOLUME_NOT_CONFIGURED = 0;
     private static final int SAFE_MEDIA_VOLUME_DISABLED = 1;
     private static final int SAFE_MEDIA_VOLUME_INACTIVE = 2;  // confirmed
     private static final int SAFE_MEDIA_VOLUME_ACTIVE = 3;  // unconfirmed
 
-    private static final int MSG_CONFIGURE_SAFE_MEDIA_VOLUME = SAFE_MEDIA_VOLUME_MSG_START + 1;
+    private static final int MSG_CONFIGURE_SAFE_MEDIA = SAFE_MEDIA_VOLUME_MSG_START + 1;
     private static final int MSG_PERSIST_SAFE_VOLUME_STATE = SAFE_MEDIA_VOLUME_MSG_START + 2;
     private static final int MSG_PERSIST_MUSIC_ACTIVE_MS = SAFE_MEDIA_VOLUME_MSG_START + 3;
+    private static final int MSG_PERSIST_CSD_VALUES = SAFE_MEDIA_VOLUME_MSG_START + 4;
 
     private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
 
@@ -100,14 +104,16 @@
     private static final int CSD_WARNING_TIMEOUT_MS_ACCUMULATION_START = -1;
     private static final int CSD_WARNING_TIMEOUT_MS_MOMENTARY_EXPOSURE = 5000;
 
+    private static final String PERSIST_CSD_RECORD_FIELD_SEPARATOR = ",";
+    private static final String PERSIST_CSD_RECORD_SEPARATOR_CHAR = "|";
+    private static final String PERSIST_CSD_RECORD_SEPARATOR = "\\|";
+
     private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE,
             "CSD updates");
 
     private int mMcc = 0;
 
-    private final boolean mEnableCsd;
-
-    final Object mSafeMediaVolumeStateLock = new Object();
+    private final Object mSafeMediaVolumeStateLock = new Object();
     private int mSafeMediaVolumeState;
 
     // Used when safe volume warning message display is requested by setStreamVolume(). In this
@@ -148,10 +154,18 @@
     @NonNull private final AudioHandler mAudioHandler;
     @NonNull private final ISafeHearingVolumeController mVolumeController;
 
+    private final boolean mEnableCsd;
+
     private ISoundDose mSoundDose;
+
+    private final Object mCsdStateLock = new Object();
+
+    @GuardedBy("mCsdStateLock")
     private float mCurrentCsd = 0.f;
     // dose at which the next dose reached warning occurs
+    @GuardedBy("mCsdStateLock")
     private float mNextCsdWarning = 1.0f;
+    @GuardedBy("mCsdStateLock")
     private final List<SoundDoseRecord> mDoseRecords = new ArrayList<>();
 
     private final Context mContext;
@@ -169,11 +183,16 @@
         }
 
         public void onNewCsdValue(float currentCsd, SoundDoseRecord[] records) {
+            if (!mEnableCsd) {
+                Log.w(TAG, "onNewCsdValue: csd not supported, ignoring value");
+                return;
+            }
+
             Log.i(TAG, "onNewCsdValue: " + currentCsd);
-            if (mCurrentCsd < currentCsd) {
-                // dose increase: going over next threshold?
-                if ((mCurrentCsd < mNextCsdWarning) && (currentCsd >= mNextCsdWarning)) {
-                    if (mEnableCsd) {
+            synchronized (mCsdStateLock) {
+                if (mCurrentCsd < currentCsd) {
+                    // dose increase: going over next threshold?
+                    if ((mCurrentCsd < mNextCsdWarning) && (currentCsd >= mNextCsdWarning)) {
                         if (mNextCsdWarning == 5.0f) {
                             // 500% dose repeat
                             mVolumeController.postDisplayCsdWarning(
@@ -188,17 +207,18 @@
                                     getTimeoutMsForWarning(
                                             AudioManager.CSD_WARNING_DOSE_REACHED_1X));
                         }
+                        mNextCsdWarning += 1.0f;
                     }
-                    mNextCsdWarning += 1.0f;
+                } else {
+                    // dose decrease: dropping below previous threshold of warning?
+                    if ((currentCsd < mNextCsdWarning - 1.0f) && (
+                            mNextCsdWarning >= 2.0f)) {
+                        mNextCsdWarning -= 1.0f;
+                    }
                 }
-            } else {
-                // dose decrease: dropping below previous threshold of warning?
-                if ((currentCsd < mNextCsdWarning - 1.0f) && (mNextCsdWarning >= 2.0f)) {
-                    mNextCsdWarning -= 1.0f;
-                }
+                mCurrentCsd = currentCsd;
+                updateSoundDoseRecords_l(records, currentCsd);
             }
-            mCurrentCsd = currentCsd;
-            updateSoundDoseRecords(records, currentCsd);
         }
     };
 
@@ -213,20 +233,22 @@
 
         mContext = context;
 
-        mSafeMediaVolumeState = mSettings.getGlobalInt(audioService.getContentResolver(),
-                Settings.Global.AUDIO_SAFE_VOLUME_STATE, 0);
-
         mEnableCsd = mContext.getResources().getBoolean(R.bool.config_audio_csd_enabled_default);
+        if (mEnableCsd) {
+            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
+        } else {
+            mSafeMediaVolumeState = mSettings.getGlobalInt(audioService.getContentResolver(),
+                    Settings.Global.AUDIO_SAFE_VOLUME_STATE, 0);
+        }
 
         // The default safe volume index read here will be replaced by the actual value when
-        // the mcc is read by onConfigureSafeVolume()
+        // the mcc is read by onConfigureSafeMedia()
+        // For now we use the same index for RS2 initial warning with CSD
         mSafeMediaVolumeIndex = mContext.getResources().getInteger(
                 R.integer.config_safe_media_volume_index) * 10;
 
         mAlarmManager = (AlarmManager) mContext.getSystemService(
                 Context.ALARM_SERVICE);
-
-        initCsd();
     }
 
     float getRs2Value() {
@@ -456,8 +478,8 @@
         }
     }
 
-    /*package*/ void configureSafeMediaVolume(boolean forced, String caller) {
-        int msg = MSG_CONFIGURE_SAFE_MEDIA_VOLUME;
+    /*package*/ void configureSafeMedia(boolean forced, String caller) {
+        int msg = MSG_CONFIGURE_SAFE_MEDIA;
         mAudioHandler.removeMessages(msg);
 
         long time = 0;
@@ -507,8 +529,8 @@
 
     /*package*/ void handleMessage(Message msg) {
         switch (msg.what) {
-            case MSG_CONFIGURE_SAFE_MEDIA_VOLUME:
-                onConfigureSafeVolume((msg.arg1 == 1), (String) msg.obj);
+            case MSG_CONFIGURE_SAFE_MEDIA:
+                onConfigureSafeMedia((msg.arg1 == 1), (String) msg.obj);
                 break;
             case MSG_PERSIST_SAFE_VOLUME_STATE:
                 onPersistSafeVolumeState(msg.arg1);
@@ -519,8 +541,13 @@
                         Settings.Secure.UNSAFE_VOLUME_MUSIC_ACTIVE_MS, musicActiveMs,
                         UserHandle.USER_CURRENT);
                 break;
+            case MSG_PERSIST_CSD_VALUES:
+                onPersistSoundDoseRecords();
+                break;
+            default:
+                Log.e(TAG, "Unexpected msg to handle: " + msg.what);
+                break;
         }
-
     }
 
     /*package*/ void dump(PrintWriter pw) {
@@ -540,33 +567,44 @@
 
     /*package*/void reset() {
         Log.d(TAG, "Reset the sound dose helper");
-        initCsd();
-    }
-
-    private void initCsd() {
-        synchronized (mSafeMediaVolumeStateLock) {
-            if (mEnableCsd) {
-                Log.v(TAG, "Initializing sound dose");
-
-                mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
-                mSoundDose = AudioSystem.getSoundDoseInterface(mSoundDoseCallback);
-                try {
-                    if (mSoundDose != null && mSoundDose.asBinder().isBinderAlive()) {
-                        if (mCurrentCsd != 0.f) {
-                            Log.d(TAG, "Resetting the saved sound dose value " + mCurrentCsd);
-                            SoundDoseRecord[] records = mDoseRecords.toArray(
-                                    new SoundDoseRecord[0]);
-                            mSoundDose.resetCsd(mCurrentCsd, records);
-                        }
+        mSoundDose = AudioSystem.getSoundDoseInterface(mSoundDoseCallback);
+        synchronized (mCsdStateLock) {
+            try {
+                if (mSoundDose != null && mSoundDose.asBinder().isBinderAlive()) {
+                    if (mCurrentCsd != 0.f) {
+                        Log.d(TAG,
+                                "Resetting the saved sound dose value " + mCurrentCsd);
+                        SoundDoseRecord[] records = mDoseRecords.toArray(
+                                new SoundDoseRecord[0]);
+                        mSoundDose.resetCsd(mCurrentCsd, records);
                     }
-                } catch (RemoteException e) {
-                    // noop
                 }
+            } catch (RemoteException e) {
+                // noop
             }
         }
     }
 
-    private void onConfigureSafeVolume(boolean force, String caller) {
+    private void initCsd() {
+        if (mEnableCsd) {
+            Log.v(TAG, "Initializing sound dose");
+
+            synchronized (mCsdStateLock) {
+                // Restore persisted values
+                mCurrentCsd = parseGlobalSettingFloat(
+                        Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE, /* defaultValue= */0.f);
+                mNextCsdWarning = parseGlobalSettingFloat(
+                        Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING, /* defaultValue= */1.f);
+                mDoseRecords.addAll(persistedStringToRecordList(
+                        mSettings.getGlobalString(mAudioService.getContentResolver(),
+                                Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS)));
+            }
+
+            reset();
+        }
+    }
+
+    private void onConfigureSafeMedia(boolean force, String caller) {
         synchronized (mSafeMediaVolumeStateLock) {
             int mcc = mContext.getResources().getConfiguration().mcc;
             if ((mMcc != mcc) || ((mMcc == 0) && force)) {
@@ -612,6 +650,10 @@
                                 /*obj=*/null), /*delay=*/0);
             }
         }
+
+        if (mEnableCsd) {
+            initCsd();
+        }
     }
 
     private int getTimeoutMsForWarning(@AudioManager.CsdWarning int csdWarning) {
@@ -702,7 +744,8 @@
         return null;
     }
 
-    private void updateSoundDoseRecords(SoundDoseRecord[] newRecords, float currentCsd) {
+    @GuardedBy("mCsdStateLock")
+    private void updateSoundDoseRecords_l(SoundDoseRecord[] newRecords, float currentCsd) {
         long totalDuration = 0;
         for (SoundDoseRecord record : newRecords) {
             Log.i(TAG, "  new record: " + record);
@@ -722,9 +765,87 @@
             }
         }
 
+        mAudioHandler.sendMessageAtTime(mAudioHandler.obtainMessage(MSG_PERSIST_CSD_VALUES,
+                /* arg1= */0, /* arg2= */0, /* obj= */null), /* delay= */0);
+
         mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration));
     }
 
+    private void onPersistSoundDoseRecords() {
+        synchronized (mCsdStateLock) {
+            mSettings.putGlobalString(mAudioService.getContentResolver(),
+                    Settings.Global.AUDIO_SAFE_CSD_CURRENT_VALUE,
+                    Float.toString(mCurrentCsd));
+            mSettings.putGlobalString(mAudioService.getContentResolver(),
+                    Settings.Global.AUDIO_SAFE_CSD_NEXT_WARNING,
+                    Float.toString(mNextCsdWarning));
+            mSettings.putGlobalString(mAudioService.getContentResolver(),
+                    Settings.Global.AUDIO_SAFE_CSD_DOSE_RECORDS,
+                    mDoseRecords.stream().map(
+                            SoundDoseHelper::recordToPersistedString).collect(
+                            Collectors.joining(PERSIST_CSD_RECORD_SEPARATOR_CHAR)));
+        }
+    }
+
+    private static String recordToPersistedString(SoundDoseRecord record) {
+        return record.timestamp
+                + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.duration
+                + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.value
+                + PERSIST_CSD_RECORD_FIELD_SEPARATOR + record.averageMel;
+    }
+
+    private static List<SoundDoseRecord> persistedStringToRecordList(String records) {
+        if (records == null || records.isEmpty()) {
+            return null;
+        }
+        return Arrays.stream(TextUtils.split(records, PERSIST_CSD_RECORD_SEPARATOR)).map(
+                SoundDoseHelper::persistedStringToRecord).filter(Objects::nonNull).collect(
+                Collectors.toList());
+    }
+
+    private static SoundDoseRecord persistedStringToRecord(String record) {
+        if (record == null || record.isEmpty()) {
+            return null;
+        }
+        final String[] fields = TextUtils.split(record, PERSIST_CSD_RECORD_FIELD_SEPARATOR);
+        if (fields.length != 4) {
+            Log.w(TAG, "Expecting 4 fields for a SoundDoseRecord, parsed " + fields.length);
+            return null;
+        }
+
+        final SoundDoseRecord sdRecord = new SoundDoseRecord();
+        try {
+            sdRecord.timestamp = Long.parseLong(fields[0]);
+            sdRecord.duration = Integer.parseInt(fields[1]);
+            sdRecord.value = Float.parseFloat(fields[2]);
+            sdRecord.averageMel = Float.parseFloat(fields[3]);
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "Unable to parse persisted SoundDoseRecord: " + record, e);
+            return null;
+        }
+
+        return sdRecord;
+    }
+
+    private float parseGlobalSettingFloat(String audioSafeCsdCurrentValue, float defaultValue) {
+        String stringValue = mSettings.getGlobalString(mAudioService.getContentResolver(),
+                audioSafeCsdCurrentValue);
+        if (stringValue == null || stringValue.isEmpty()) {
+            Log.v(TAG, "No value stored in settings " + audioSafeCsdCurrentValue);
+            return defaultValue;
+        }
+
+        float value;
+        try {
+            value = Float.parseFloat(stringValue);
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "Error parsing float from settings " + audioSafeCsdCurrentValue, e);
+            value = defaultValue;
+        }
+
+        return value;
+    }
+
     // StreamVolumeCommand contains the information needed to defer the process of
     // setStreamVolume() in case the user has to acknowledge the safe volume warning message.
     private static class StreamVolumeCommand {
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 064cd2d..ba96861 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -25,6 +25,7 @@
 import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
 import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE;
 import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_EMULATED_STATE;
+import static com.android.server.devicestate.OverrideRequestController.FLAG_THERMAL_CRITICAL;
 import static com.android.server.devicestate.OverrideRequestController.STATUS_ACTIVE;
 import static com.android.server.devicestate.OverrideRequestController.STATUS_CANCELED;
 import static com.android.server.devicestate.OverrideRequestController.STATUS_UNKNOWN;
@@ -183,6 +184,9 @@
     ActivityTaskManagerInternal.ScreenObserver mOverrideRequestScreenObserver =
             new OverrideRequestScreenObserver();
 
+    @NonNull
+    private final DeviceStateNotificationController mDeviceStateNotificationController;
+
     public DeviceStateManagerService(@NonNull Context context) {
         this(context, DeviceStatePolicy.Provider
                 .fromResources(context.getResources())
@@ -211,6 +215,13 @@
         mDeviceStatePolicy.getDeviceStateProvider().setListener(mDeviceStateProviderListener);
         mBinderService = new BinderService();
         mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+        mDeviceStateNotificationController = new DeviceStateNotificationController(
+                context, mHandler,
+                () -> {
+                    synchronized (mLock) {
+                        mActiveOverride.ifPresent(mOverrideRequestController::cancelRequest);
+                    }
+                });
     }
 
     @Override
@@ -346,7 +357,8 @@
         return mBinderService;
     }
 
-    private void updateSupportedStates(DeviceState[] supportedDeviceStates) {
+    private void updateSupportedStates(DeviceState[] supportedDeviceStates,
+            @DeviceStateProvider.SupportedStatesUpdatedReason int reason) {
         synchronized (mLock) {
             final int[] oldStateIdentifiers = getSupportedStateIdentifiersLocked();
 
@@ -370,7 +382,7 @@
                 return;
             }
 
-            mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers);
+            mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers, reason);
             updatePendingStateLocked();
 
             setRearDisplayStateLocked();
@@ -596,7 +608,7 @@
 
     @GuardedBy("mLock")
     private void onOverrideRequestStatusChangedLocked(@NonNull OverrideRequest request,
-            @OverrideRequestController.RequestStatus int status) {
+            @OverrideRequestController.RequestStatus int status, int flags) {
         if (request.getRequestType() == OVERRIDE_REQUEST_TYPE_BASE_STATE) {
             switch (status) {
                 case STATUS_ACTIVE:
@@ -616,10 +628,19 @@
             switch (status) {
                 case STATUS_ACTIVE:
                     mActiveOverride = Optional.of(request);
+                    mDeviceStateNotificationController.showStateActiveNotificationIfNeeded(
+                            request.getRequestedState(), request.getUid());
                     break;
                 case STATUS_CANCELED:
                     if (mActiveOverride.isPresent() && mActiveOverride.get() == request) {
                         mActiveOverride = Optional.empty();
+                        mDeviceStateNotificationController.cancelNotification(
+                                request.getRequestedState());
+                        if ((flags & FLAG_THERMAL_CRITICAL) == FLAG_THERMAL_CRITICAL) {
+                            mDeviceStateNotificationController
+                                    .showThermalCriticalNotificationIfNeeded(
+                                            request.getRequestedState());
+                        }
                     }
                     break;
                 case STATUS_UNKNOWN: // same as default
@@ -700,7 +721,7 @@
         }
     }
 
-    private void requestStateInternal(int state, int flags, int callingPid,
+    private void requestStateInternal(int state, int flags, int callingPid, int callingUid,
             @NonNull IBinder token, boolean hasControlDeviceStatePermission) {
         synchronized (mLock) {
             final ProcessRecord processRecord = mProcessRecords.get(callingPid);
@@ -721,8 +742,8 @@
                         + " is not supported.");
             }
 
-            OverrideRequest request = new OverrideRequest(token, callingPid, state, flags,
-                    OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
+            OverrideRequest request = new OverrideRequest(token, callingPid, callingUid,
+                    state, flags, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
 
             // If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay
             if (!hasControlDeviceStatePermission && mRearDisplayState != null
@@ -762,7 +783,7 @@
     }
 
     private void requestBaseStateOverrideInternal(int state, int flags, int callingPid,
-            @NonNull IBinder token) {
+            int callingUid, @NonNull IBinder token) {
         synchronized (mLock) {
             final Optional<DeviceState> deviceState = getStateLocked(state);
             if (!deviceState.isPresent()) {
@@ -782,8 +803,8 @@
                         + " token: " + token);
             }
 
-            OverrideRequest request = new OverrideRequest(token, callingPid, state, flags,
-                    OVERRIDE_REQUEST_TYPE_BASE_STATE);
+            OverrideRequest request = new OverrideRequest(token, callingPid, callingUid,
+                    state, flags, OVERRIDE_REQUEST_TYPE_BASE_STATE);
             mOverrideRequestController.addBaseStateRequest(request);
         }
     }
@@ -953,11 +974,12 @@
         @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int mCurrentBaseState;
 
         @Override
-        public void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates) {
+        public void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates,
+                @DeviceStateProvider.SupportedStatesUpdatedReason int reason) {
             if (newDeviceStates.length == 0) {
                 throw new IllegalArgumentException("Supported device states must not be empty");
             }
-            updateSupportedStates(newDeviceStates);
+            updateSupportedStates(newDeviceStates, reason);
         }
 
         @Override
@@ -1085,6 +1107,7 @@
         @Override // Binder call
         public void requestState(IBinder token, int state, int flags) {
             final int callingPid = Binder.getCallingPid();
+            final int callingUid = Binder.getCallingUid();
             // Allow top processes to request a device state change
             // If the calling process ID is not the top app, then we check if this process
             // holds a permission to CONTROL_DEVICE_STATE
@@ -1099,7 +1122,8 @@
 
             final long callingIdentity = Binder.clearCallingIdentity();
             try {
-                requestStateInternal(state, flags, callingPid, token, hasControlStatePermission);
+                requestStateInternal(state, flags, callingPid, callingUid, token,
+                        hasControlStatePermission);
             } finally {
                 Binder.restoreCallingIdentity(callingIdentity);
             }
@@ -1124,6 +1148,7 @@
         @Override // Binder call
         public void requestBaseStateOverride(IBinder token, int state, int flags) {
             final int callingPid = Binder.getCallingPid();
+            final int callingUid = Binder.getCallingUid();
             getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE,
                     "Permission required to control base state of device.");
 
@@ -1133,7 +1158,7 @@
 
             final long callingIdentity = Binder.clearCallingIdentity();
             try {
-                requestBaseStateOverrideInternal(state, flags, callingPid, token);
+                requestBaseStateOverrideInternal(state, flags, callingPid, callingUid, token);
             } finally {
                 Binder.restoreCallingIdentity(callingIdentity);
             }
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java b/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java
new file mode 100644
index 0000000..2f14998
--- /dev/null
+++ b/services/core/java/com/android/server/devicestate/DeviceStateNotificationController.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicestate;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Manages the user-visible device state notifications.
+ */
+class DeviceStateNotificationController extends BroadcastReceiver {
+    private static final String TAG = "DeviceStateNotificationController";
+
+    @VisibleForTesting static final String INTENT_ACTION_CANCEL_STATE =
+            "com.android.server.devicestate.INTENT_ACTION_CANCEL_STATE";
+    @VisibleForTesting static final int NOTIFICATION_ID = 1;
+    @VisibleForTesting static final String CHANNEL_ID = "DeviceStateManager";
+    @VisibleForTesting static final String NOTIFICATION_TAG = "DeviceStateManager";
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final NotificationManager mNotificationManager;
+    private final PackageManager mPackageManager;
+
+    // Stores the notification title and content indexed with the device state identifier.
+    private final SparseArray<NotificationInfo> mNotificationInfos;
+
+    // The callback when a device state is requested to be canceled.
+    private final Runnable mCancelStateRunnable;
+
+    DeviceStateNotificationController(@NonNull Context context, @NonNull Handler handler,
+            @NonNull Runnable cancelStateRunnable) {
+        this(context, handler, cancelStateRunnable, getNotificationInfos(context),
+                context.getPackageManager(), context.getSystemService(NotificationManager.class));
+    }
+
+    @VisibleForTesting
+    DeviceStateNotificationController(
+            @NonNull Context context, @NonNull Handler handler,
+            @NonNull Runnable cancelStateRunnable,
+            @NonNull SparseArray<NotificationInfo> notificationInfos,
+            @NonNull PackageManager packageManager,
+            @NonNull NotificationManager notificationManager) {
+        mContext = context;
+        mHandler = handler;
+        mCancelStateRunnable = cancelStateRunnable;
+        mNotificationInfos = notificationInfos;
+        mPackageManager = packageManager;
+        mNotificationManager = notificationManager;
+        mContext.registerReceiver(
+                this,
+                new IntentFilter(INTENT_ACTION_CANCEL_STATE),
+                android.Manifest.permission.CONTROL_DEVICE_STATE,
+                mHandler,
+                Context.RECEIVER_NOT_EXPORTED);
+    }
+
+    /**
+     * Displays the ongoing notification indicating that the device state is active. Does nothing if
+     * the state does not have an active notification.
+     *
+     * @param state the active device state identifier.
+     * @param requestingAppUid the uid of the requesting app used to retrieve the app name.
+     */
+    void showStateActiveNotificationIfNeeded(int state, int requestingAppUid) {
+        NotificationInfo info = mNotificationInfos.get(state);
+        if (info == null || !info.hasActiveNotification()) {
+            return;
+        }
+        String requesterApplicationLabel = getApplicationLabel(requestingAppUid);
+        if (requesterApplicationLabel != null) {
+            showNotification(
+                    info.name, info.activeNotificationTitle,
+                    String.format(info.activeNotificationContent, requesterApplicationLabel),
+                    true /* ongoing */
+            );
+        } else {
+            Slog.e(TAG, "Cannot determine the requesting app name when showing state active "
+                    + "notification. uid=" + requestingAppUid + ", state=" + state);
+        }
+    }
+
+    /**
+     * Displays the notification indicating that the device state is canceled due to thermal
+     * critical condition. Does nothing if the state does not have a thermal critical notification.
+     *
+     * @param state the identifier of the device state being canceled.
+     */
+    void showThermalCriticalNotificationIfNeeded(int state) {
+        NotificationInfo info = mNotificationInfos.get(state);
+        if (info == null || !info.hasThermalCriticalNotification()) {
+            return;
+        }
+        showNotification(
+                info.name, info.thermalCriticalNotificationTitle,
+                info.thermalCriticalNotificationContent, false /* ongoing */
+        );
+    }
+
+    /**
+     * Cancels the notification of the corresponding device state.
+     *
+     * @param state the device state identifier.
+     */
+    void cancelNotification(int state) {
+        if (!mNotificationInfos.contains(state)) {
+            return;
+        }
+        mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+    }
+
+    @Override
+    public void onReceive(@NonNull Context context, @Nullable Intent intent) {
+        if (intent != null) {
+            if (INTENT_ACTION_CANCEL_STATE.equals(intent.getAction())) {
+                mCancelStateRunnable.run();
+            }
+        }
+    }
+
+    /**
+     * Displays a notification with the specified name, title, and content.
+     *
+     * @param name the name of the notification.
+     * @param title the title of the notification.
+     * @param content the content of the notification.
+     * @param ongoing if true, display an ongoing (sticky) notification with a turn off button.
+     */
+    private void showNotification(
+            @NonNull String name, @NonNull String title, @NonNull String content, boolean ongoing) {
+        final NotificationChannel channel = new NotificationChannel(
+                CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH);
+        final Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID)
+                .setSmallIcon(R.drawable.ic_lock) // TODO(b/266833171) update icons when available.
+                .setContentTitle(title)
+                .setContentText(content)
+                .setSubText(name)
+                .setLocalOnly(true)
+                .setOngoing(ongoing)
+                .setCategory(Notification.CATEGORY_SYSTEM);
+
+        if (ongoing) {
+            final Intent intent = new Intent(INTENT_ACTION_CANCEL_STATE)
+                    .setPackage(mContext.getPackageName());
+            final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                    mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
+            final Notification.Action action = new Notification.Action.Builder(
+                    null /* icon */,
+                    mContext.getString(R.string.device_state_notification_turn_off_button),
+                    pendingIntent)
+                    .build();
+            builder.addAction(action);
+        }
+
+        mNotificationManager.createNotificationChannel(channel);
+        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build());
+    }
+
+    /**
+     * Loads the resources for the notifications. The device state identifiers and strings are
+     * stored in arrays. All the string arrays must have the same length and same order as the
+     * identifier array.
+     */
+    private static SparseArray<NotificationInfo> getNotificationInfos(Context context) {
+        final SparseArray<NotificationInfo> notificationInfos = new SparseArray<>();
+
+        final int[] stateIdentifiers =
+                context.getResources().getIntArray(
+                        R.array.device_state_notification_state_identifiers);
+        final String[] names =
+                context.getResources().getStringArray(R.array.device_state_notification_names);
+        final String[] activeNotificationTitles =
+                context.getResources().getStringArray(
+                        R.array.device_state_notification_active_titles);
+        final String[] activeNotificationContents =
+                context.getResources().getStringArray(
+                        R.array.device_state_notification_active_contents);
+        final String[] thermalCriticalNotificationTitles =
+                context.getResources().getStringArray(
+                        R.array.device_state_notification_thermal_titles);
+        final String[] thermalCriticalNotificationContents =
+                context.getResources().getStringArray(
+                        R.array.device_state_notification_thermal_contents);
+
+        if (stateIdentifiers.length != names.length
+                || stateIdentifiers.length != activeNotificationTitles.length
+                || stateIdentifiers.length != activeNotificationContents.length
+                || stateIdentifiers.length != thermalCriticalNotificationTitles.length
+                || stateIdentifiers.length != thermalCriticalNotificationContents.length
+        ) {
+            throw new IllegalStateException(
+                    "The length of state identifiers and notification texts must match!");
+        }
+
+        for (int i = 0; i < stateIdentifiers.length; i++) {
+            int identifier = stateIdentifiers[i];
+            if (identifier == DeviceStateManager.INVALID_DEVICE_STATE) {
+                continue;
+            }
+
+            notificationInfos.put(
+                    identifier,
+                    new NotificationInfo(
+                            names[i], activeNotificationTitles[i], activeNotificationContents[i],
+                            thermalCriticalNotificationTitles[i],
+                            thermalCriticalNotificationContents[i])
+            );
+        }
+
+        return notificationInfos;
+    }
+
+    /**
+     * A helper function to get app name (label) using the app uid.
+     *
+     * @param uid the uid of the app.
+     * @return app name (label) if found, or null otherwise.
+     */
+    @Nullable
+    private String getApplicationLabel(int uid) {
+        String packageName = mPackageManager.getNameForUid(uid);
+        try {
+            ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
+                    packageName, PackageManager.ApplicationInfoFlags.of(0));
+            return appInfo.loadLabel(mPackageManager).toString();
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
+        }
+    }
+
+    /**
+     * A data class storing string resources of the notification of a device state.
+     */
+    @VisibleForTesting
+    static class NotificationInfo {
+        public final String name;
+        public final String activeNotificationTitle;
+        public final String activeNotificationContent;
+        public final String thermalCriticalNotificationTitle;
+        public final String thermalCriticalNotificationContent;
+
+        NotificationInfo(String name, String activeNotificationTitle,
+                String activeNotificationContent, String thermalCriticalNotificationTitle,
+                String thermalCriticalNotificationContent) {
+
+            this.name = name;
+            this.activeNotificationTitle = activeNotificationTitle;
+            this.activeNotificationContent = activeNotificationContent;
+            this.thermalCriticalNotificationTitle = thermalCriticalNotificationTitle;
+            this.thermalCriticalNotificationContent = thermalCriticalNotificationContent;
+        }
+
+        boolean hasActiveNotification() {
+            return activeNotificationTitle != null && activeNotificationTitle.length() > 0;
+        }
+
+        boolean hasThermalCriticalNotification() {
+            return thermalCriticalNotificationTitle != null
+                    && thermalCriticalNotificationTitle.length() > 0;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index 109bf63..fecc13f 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -19,8 +19,12 @@
 import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
 import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
 
+import android.annotation.IntDef;
 import android.annotation.IntRange;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Responsible for providing the set of supported {@link DeviceState device states} as well as the
  * current device state.
@@ -28,12 +32,42 @@
  * @see DeviceStatePolicy
  */
 public interface DeviceStateProvider {
+    int SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT = 0;
+
+    /**
+     * Indicating that the supported device states changed callback is trigger for initial listener
+     * registration.
+     */
+    int SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED = 1;
+
+    /**
+     * Indicating that the supported device states have changed because the thermal condition
+     * returned to normal status from critical status.
+     */
+    int SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL = 2;
+
+    /**
+     * Indicating that the supported device states have changed because of thermal critical
+     * condition.
+     */
+    int SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL = 3;
+
+    @IntDef(prefix = { "SUPPORTED_DEVICE_STATES_CHANGED_" }, value = {
+            SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT,
+            SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED,
+            SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL,
+            SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface SupportedStatesUpdatedReason {}
+
     /**
      * Registers a listener for changes in provider state.
      * <p>
-     * It is <b>required</b> that {@link Listener#onSupportedDeviceStatesChanged(DeviceState[])} be
-     * called followed by {@link Listener#onStateChanged(int)} with the initial values on successful
-     * registration of the listener.
+     * It is <b>required</b> that
+     * {@link Listener#onSupportedDeviceStatesChanged(DeviceState[], int)} be called followed by
+     * {@link Listener#onStateChanged(int)} with the initial values on successful registration of
+     * the listener.
      */
     void setListener(Listener listener);
 
@@ -53,18 +87,20 @@
          * to zero and there must always be at least one supported device state.
          *
          * @param newDeviceStates array of supported device states.
+         * @param reason the reason for the supported device states change.
          *
          * @throws IllegalArgumentException if the list of device states is empty or if one of the
          * provided states contains an invalid identifier.
          */
-        void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates);
+        void onSupportedDeviceStatesChanged(DeviceState[] newDeviceStates,
+                @SupportedStatesUpdatedReason int reason);
 
         /**
          * Called to notify the listener of a change in current device state. Required to be called
          * once on successful registration of the listener and then once on every subsequent change
          * in device state. Value must have been included in the set of supported device states
          * provided in the most recent call to
-         * {@link #onSupportedDeviceStatesChanged(DeviceState[])}.
+         * {@link #onSupportedDeviceStatesChanged(DeviceState[], int)}.
          *
          * @param identifier the identifier of the new device state.
          *
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequest.java b/services/core/java/com/android/server/devicestate/OverrideRequest.java
index 325e384..74cf184 100644
--- a/services/core/java/com/android/server/devicestate/OverrideRequest.java
+++ b/services/core/java/com/android/server/devicestate/OverrideRequest.java
@@ -31,6 +31,7 @@
 final class OverrideRequest {
     private final IBinder mToken;
     private final int mPid;
+    private final int mUid;
     private final int mRequestedState;
     @DeviceStateRequest.RequestFlags
     private final int mFlags;
@@ -68,10 +69,11 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface OverrideRequestType {}
 
-    OverrideRequest(IBinder token, int pid, int requestedState,
+    OverrideRequest(IBinder token, int pid, int uid, int requestedState,
             @DeviceStateRequest.RequestFlags int flags, @OverrideRequestType int requestType) {
         mToken = token;
         mPid = pid;
+        mUid = uid;
         mRequestedState = requestedState;
         mFlags = flags;
         mRequestType = requestType;
@@ -85,6 +87,10 @@
         return mPid;
     }
 
+    int getUid() {
+        return mUid;
+    }
+
     int getRequestedState() {
         return mRequestedState;
     }
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequestController.java b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
index e39c732..2ed4765 100644
--- a/services/core/java/com/android/server/devicestate/OverrideRequestController.java
+++ b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
@@ -59,6 +59,11 @@
     @Retention(RetentionPolicy.SOURCE)
     @interface RequestStatus {}
 
+    /**
+     * A flag indicating that the status change was triggered by thermal critical status.
+     */
+    static final int FLAG_THERMAL_CRITICAL = 1 << 0;
+
     static String statusToString(@RequestStatus int status) {
         switch (status) {
             case STATUS_ACTIVE:
@@ -106,7 +111,7 @@
     void addRequest(@NonNull OverrideRequest request) {
         OverrideRequest previousRequest = mRequest;
         mRequest = request;
-        mListener.onStatusChanged(request, STATUS_ACTIVE);
+        mListener.onStatusChanged(request, STATUS_ACTIVE, 0 /* flags */);
 
         if (previousRequest != null) {
             cancelRequestLocked(previousRequest);
@@ -116,7 +121,7 @@
     void addBaseStateRequest(@NonNull OverrideRequest request) {
         OverrideRequest previousRequest = mBaseStateRequest;
         mBaseStateRequest = request;
-        mListener.onStatusChanged(request, STATUS_ACTIVE);
+        mListener.onStatusChanged(request, STATUS_ACTIVE, 0 /* flags */);
 
         if (previousRequest != null) {
             cancelRequestLocked(previousRequest);
@@ -219,14 +224,17 @@
      * Notifies the controller that the set of supported states has changed. The controller will
      * notify the listener of all changes to request status as a result of this change.
      */
-    void handleNewSupportedStates(int[] newSupportedStates) {
+    void handleNewSupportedStates(int[] newSupportedStates,
+            @DeviceStateProvider.SupportedStatesUpdatedReason int reason) {
+        boolean isThermalCritical =
+                reason == DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
         if (mBaseStateRequest != null && !contains(newSupportedStates,
                 mBaseStateRequest.getRequestedState())) {
-            cancelCurrentBaseStateRequestLocked();
+            cancelCurrentBaseStateRequestLocked(isThermalCritical ? FLAG_THERMAL_CRITICAL : 0);
         }
 
         if (mRequest != null && !contains(newSupportedStates, mRequest.getRequestedState())) {
-            cancelCurrentRequestLocked();
+            cancelCurrentRequestLocked(isThermalCritical ? FLAG_THERMAL_CRITICAL : 0);
         }
     }
 
@@ -244,7 +252,11 @@
     }
 
     private void cancelRequestLocked(@NonNull OverrideRequest requestToCancel) {
-        mListener.onStatusChanged(requestToCancel, STATUS_CANCELED);
+        cancelRequestLocked(requestToCancel, 0 /* flags */);
+    }
+
+    private void cancelRequestLocked(@NonNull OverrideRequest requestToCancel, int flags) {
+        mListener.onStatusChanged(requestToCancel, STATUS_CANCELED, flags);
     }
 
     /**
@@ -252,12 +264,16 @@
      * Notifies the listener of the canceled status as well.
      */
     private void cancelCurrentRequestLocked() {
+        cancelCurrentRequestLocked(0 /* flags */);
+    }
+
+    private void cancelCurrentRequestLocked(int flags) {
         if (mRequest == null) {
             Slog.w(TAG, "Attempted to cancel a null OverrideRequest");
             return;
         }
         mStickyRequest = false;
-        cancelRequestLocked(mRequest);
+        cancelRequestLocked(mRequest, flags);
         mRequest = null;
     }
 
@@ -266,11 +282,15 @@
      * Notifies the listener of the canceled status as well.
      */
     private void cancelCurrentBaseStateRequestLocked() {
+        cancelCurrentBaseStateRequestLocked(0 /* flags */);
+    }
+
+    private void cancelCurrentBaseStateRequestLocked(int flags) {
         if (mBaseStateRequest == null) {
             Slog.w(TAG, "Attempted to cancel a null OverrideRequest");
             return;
         }
-        cancelRequestLocked(mBaseStateRequest);
+        cancelRequestLocked(mBaseStateRequest, flags);
         mBaseStateRequest = null;
     }
 
@@ -284,12 +304,14 @@
     }
 
     public interface StatusChangeListener {
+
         /**
          * Notifies the listener of a change in request status. If a change within the controller
          * causes one request to become active and one to become either suspended or cancelled, this
          * method is guaranteed to be called with the active request first before the suspended or
          * cancelled request.
          */
-        void onStatusChanged(@NonNull OverrideRequest request, @RequestStatus int newStatus);
+        void onStatusChanged(@NonNull OverrideRequest request, @RequestStatus int newStatus,
+                int flags);
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java
index fa9a100..f229d0f 100644
--- a/services/core/java/com/android/server/display/DisplayControl.java
+++ b/services/core/java/com/android/server/display/DisplayControl.java
@@ -28,7 +28,8 @@
  * Calls into SurfaceFlinger for Display creation and deletion.
  */
 public class DisplayControl {
-    private static native IBinder nativeCreateDisplay(String name, boolean secure);
+    private static native IBinder nativeCreateDisplay(String name, boolean secure,
+            float requestedRefreshRate);
     private static native void nativeDestroyDisplay(IBinder displayToken);
     private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes);
     private static native long[] nativeGetPhysicalDisplayIds();
@@ -46,7 +47,25 @@
      */
     public static IBinder createDisplay(String name, boolean secure) {
         Objects.requireNonNull(name, "name must not be null");
-        return nativeCreateDisplay(name, secure);
+        return nativeCreateDisplay(name, secure, 0.0f);
+    }
+
+    /**
+     * Create a display in SurfaceFlinger.
+     *
+     * @param name The name of the display
+     * @param secure Whether this display is secure.
+     * @param requestedRefreshRate The requested refresh rate in frames per second.
+     * For best results, specify a divisor of the physical refresh rate, e.g., 30 or 60 on
+     * 120hz display. If an arbitrary refresh rate is specified, the rate will be rounded
+     * up or down to a divisor of the physical display. If 0 is specified, the virtual
+     * display is refreshed at the physical display refresh rate.
+     * @return The token reference for the display in SurfaceFlinger.
+     */
+    public static IBinder createDisplay(String name, boolean secure,
+            float requestedRefreshRate) {
+        Objects.requireNonNull(name, "name must not be null");
+        return nativeCreateDisplay(name, secure, requestedRefreshRate);
     }
 
     /**
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index ddeaa1b..364d53b 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -106,7 +106,8 @@
         String name = virtualDisplayConfig.getName();
         boolean secure = (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0;
         IBinder appToken = callback.asBinder();
-        IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure);
+        IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure,
+                virtualDisplayConfig.getRequestedRefreshRate());
         final String baseUniqueId =
                 UNIQUE_ID_PREFIX + ownerPackageName + "," + ownerUid + "," + name + ",";
         final int uniqueIndex = getNextUniqueIndex(baseUniqueId);
@@ -247,6 +248,7 @@
         private int mWidth;
         private int mHeight;
         private int mDensityDpi;
+        private float mRequestedRefreshRate;
         private Surface mSurface;
         private DisplayDeviceInfo mInfo;
         private int mDisplayState;
@@ -270,8 +272,9 @@
             mName = virtualDisplayConfig.getName();
             mWidth = virtualDisplayConfig.getWidth();
             mHeight = virtualDisplayConfig.getHeight();
-            mMode = createMode(mWidth, mHeight, REFRESH_RATE);
             mDensityDpi = virtualDisplayConfig.getDensityDpi();
+            mRequestedRefreshRate = virtualDisplayConfig.getRequestedRefreshRate();
+            mMode = createMode(mWidth, mHeight, getRefreshRate());
             mSurface = surface;
             mFlags = flags;
             mCallback = callback;
@@ -410,7 +413,7 @@
                 sendTraversalRequestLocked();
                 mWidth = width;
                 mHeight = height;
-                mMode = createMode(width, height, REFRESH_RATE);
+                mMode = createMode(width, height, getRefreshRate());
                 mDensityDpi = densityDpi;
                 mInfo = null;
                 mPendingChanges |= PENDING_RESIZE;
@@ -438,9 +441,9 @@
             pw.println("mStopped=" + mStopped);
             pw.println("mDisplayIdToMirror=" + mDisplayIdToMirror);
             pw.println("mWindowManagerMirroring=" + mIsWindowManagerMirroring);
+            pw.println("mRequestedRefreshRate=" + mRequestedRefreshRate);
         }
 
-
         @Override
         public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
             if (mInfo == null) {
@@ -456,7 +459,7 @@
                 mInfo.densityDpi = mDensityDpi;
                 mInfo.xDpi = mDensityDpi;
                 mInfo.yDpi = mDensityDpi;
-                mInfo.presentationDeadlineNanos = 1000000000L / (int) REFRESH_RATE; // 1 frame
+                mInfo.presentationDeadlineNanos = 1000000000L / (int) getRefreshRate(); // 1 frame
                 mInfo.flags = 0;
                 if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) {
                     mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE
@@ -554,6 +557,10 @@
             }
             return mInfo;
         }
+
+        private float getRefreshRate() {
+            return (mRequestedRefreshRate != 0.0f) ? mRequestedRefreshRate : REFRESH_RATE;
+        }
     }
 
     private static class Callback extends Handler {
@@ -632,6 +639,19 @@
 
     @VisibleForTesting
     public interface SurfaceControlDisplayFactory {
-        public IBinder createDisplay(String name, boolean secure);
+        /**
+         * Create a virtual display in SurfaceFlinger.
+         *
+         * @param name The name of the display
+         * @param secure Whether this display is secure.
+         * @param requestedRefreshRate
+         *     The refresh rate, frames per second, to request on the virtual display.
+         *     It should be a divisor of refresh rate of the leader physical display
+         *     that drives VSYNC, e.g. 30/60fps on 120fps display. If an arbitrary refresh
+         *     rate is specified, SurfaceFlinger rounds up or down to match a divisor of
+         *     the refresh rate of the leader physical display.
+         * @return The token reference for the display in SurfaceFlinger.
+         */
+        IBinder createDisplay(String name, boolean secure, float requestedRefreshRate);
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index ffc309e..3c97aaf8 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -280,13 +280,13 @@
     public void setRouteListingPreference(
             @NonNull IMediaRouter2 router,
             @Nullable RouteListingPreference routeListingPreference) {
-        ComponentName inAppOnlyItemRoutingReceiver =
+        ComponentName linkedItemLandingComponent =
                 routeListingPreference != null
-                        ? routeListingPreference.getInAppOnlyItemRoutingReceiver()
+                        ? routeListingPreference.getLinkedItemComponentName()
                         : null;
-        if (inAppOnlyItemRoutingReceiver != null) {
+        if (linkedItemLandingComponent != null) {
             MediaServerUtils.enforcePackageName(
-                    inAppOnlyItemRoutingReceiver.getPackageName(), Binder.getCallingUid());
+                    linkedItemLandingComponent.getPackageName(), Binder.getCallingUid());
         }
 
         final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index 69436da..8d40adf 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -27,12 +27,15 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
+import android.os.Build;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
@@ -51,6 +54,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Set;
@@ -122,7 +126,18 @@
         @Override
         public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
                 @PackageManager.PackageInfoFlagsBits long flags, int userId) {
-            return mService.getBackgroundInstalledPackages(flags, userId);
+            if (!Build.IS_DEBUGGABLE) {
+                return mService.getBackgroundInstalledPackages(flags, userId);
+            }
+            // The debug.transparency.bg-install-apps (only works for debuggable builds)
+            // is used to set mock list of background installed apps for testing.
+            // The list of apps' names is delimited by ",".
+            String propertyString = SystemProperties.get("debug.transparency.bg-install-apps");
+            if (TextUtils.isEmpty(propertyString)) {
+                return mService.getBackgroundInstalledPackages(flags, userId);
+            } else {
+                return mService.getMockBackgroundInstalledPackages(propertyString);
+            }
         }
     }
 
@@ -145,6 +160,27 @@
         return new ParceledListSlice<>(packages);
     }
 
+    /**
+     * Mock a list of background installed packages based on the property string.
+     */
+    @NonNull
+    ParceledListSlice<PackageInfo> getMockBackgroundInstalledPackages(
+            @NonNull String propertyString) {
+        String[] mockPackageNames = propertyString.split(",");
+        List<PackageInfo> mockPackages = new ArrayList<>();
+        for (String name : mockPackageNames) {
+            try {
+                PackageInfo packageInfo = mPackageManager.getPackageInfo(name,
+                        PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL));
+                mockPackages.add(packageInfo);
+            } catch (PackageManager.NameNotFoundException e) {
+                Slog.w(TAG, "Package's PackageInfo not found " + name);
+                continue;
+            }
+        }
+        return new ParceledListSlice<>(mockPackages);
+    }
+
     private static class EventHandler extends Handler {
         private final BackgroundInstallControlService mService;
 
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index bd6bd75..0d5392b 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -70,6 +70,7 @@
 import static com.android.server.pm.PackageManagerService.SCAN_AS_OEM;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_PRIVILEGED;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_PRODUCT;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_STOPPED_SYSTEM_APP;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM_EXT;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_VENDOR;
@@ -4237,6 +4238,18 @@
             }
         }
 
+        // A new application appeared on /system, and we are seeing it for the first time.
+        // Its also not updated as we don't have a copy of it on /data. So, scan it in a
+        // STOPPED state. Ignore if it's an APEX package since stopped state does not affect them.
+        final boolean isApexPkg = (scanFlags & SCAN_AS_APEX) != 0;
+        if (mPm.mShouldStopSystemPackagesByDefault && scanSystemPartition
+                && !pkgAlreadyExists && !isApexPkg) {
+            String packageName = parsedPackage.getPackageName();
+            if (!mPm.mInitialNonStoppedSystemPackages.contains(packageName)) {
+                scanFlags |= SCAN_AS_STOPPED_SYSTEM_APP;
+            }
+        }
+
         final ScanResult scanResult = scanPackageNewLI(parsedPackage, parseFlags,
                 scanFlags | SCAN_UPDATE_SIGNATURE, 0 /* currentTime */, user, null);
         return new Pair<>(scanResult, shouldHideSystemApp);
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 617561a..d23ea16 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -706,7 +706,7 @@
             }
         }
 
-        if (Build.IS_DEBUGGABLE || isCalledBySystemOrShell(callingUid)) {
+        if (Build.IS_DEBUGGABLE || isCalledBySystem(callingUid)) {
             params.installFlags |= PackageManager.INSTALL_ALLOW_DOWNGRADE;
         } else {
             params.installFlags &= ~PackageManager.INSTALL_ALLOW_DOWNGRADE;
@@ -916,6 +916,10 @@
         return sessionId;
     }
 
+    private static boolean isCalledBySystem(int callingUid) {
+        return callingUid == Process.SYSTEM_UID || callingUid == Process.ROOT_UID;
+    }
+
     private boolean isCalledBySystemOrShell(int callingUid) {
         return callingUid == Process.SYSTEM_UID || callingUid == Process.ROOT_UID
                 || callingUid == Process.SHELL_UID;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index a02a419..c6a1579 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -387,6 +387,7 @@
     static final int SCAN_DROP_CACHE = 1 << 24;
     static final int SCAN_AS_FACTORY = 1 << 25;
     static final int SCAN_AS_APEX = 1 << 26;
+    static final int SCAN_AS_STOPPED_SYSTEM_APP = 1 << 27;
 
     @IntDef(flag = true, prefix = { "SCAN_" }, value = {
             SCAN_NO_DEX,
@@ -403,6 +404,7 @@
             SCAN_AS_INSTANT_APP,
             SCAN_AS_FULL_APP,
             SCAN_AS_VIRTUAL_PRELOAD,
+            SCAN_AS_STOPPED_SYSTEM_APP,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ScanFlags {}
@@ -964,6 +966,8 @@
     final @Nullable String mRecentsPackage;
     final @Nullable String mAmbientContextDetectionPackage;
     final @Nullable String mWearableSensingPackage;
+    final @NonNull Set<String> mInitialNonStoppedSystemPackages;
+    final boolean mShouldStopSystemPackagesByDefault;
     private final @NonNull String mRequiredSdkSandboxPackage;
 
     @GuardedBy("mLock")
@@ -1771,6 +1775,8 @@
         mOverlayConfigSignaturePackage = testParams.overlayConfigSignaturePackage;
         mResolveComponentName = testParams.resolveComponentName;
         mRequiredSdkSandboxPackage = testParams.requiredSdkSandboxPackage;
+        mInitialNonStoppedSystemPackages = testParams.initialNonStoppedSystemPackages;
+        mShouldStopSystemPackagesByDefault = testParams.shouldStopSystemPackagesByDefault;
 
         mLiveComputer = createLiveComputer();
         mSnapshotStatistics = null;
@@ -2097,6 +2103,11 @@
             mCacheDir = PackageManagerServiceUtils.preparePackageParserCache(
                     mIsEngBuild, mIsUserDebugBuild, mIncrementalVersion);
 
+            mInitialNonStoppedSystemPackages = mInjector.getSystemConfig()
+                    .getInitialNonStoppedSystemPackages();
+            mShouldStopSystemPackagesByDefault = mContext.getResources()
+                    .getBoolean(R.bool.config_stopSystemPackagesByDefault);
+
             final int[] userIds = mUserManager.getUserIds();
             PackageParser2 packageParser = mInjector.getScanningCachingPackageParser();
             mOverlayConfig = mInitAppsHelper.initSystemApps(packageParser, packageSettings, userIds,
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
index e5cfa67..0ed90e4 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
@@ -26,6 +26,7 @@
 import android.os.Handler;
 import android.os.incremental.IncrementalManager;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.DisplayMetrics;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -40,6 +41,7 @@
 
 import java.io.File;
 import java.util.List;
+import java.util.Set;
 
 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
 public final class PackageManagerServiceTestParams {
@@ -119,4 +121,6 @@
     public SuspendPackageHelper suspendPackageHelper;
     public DistractingPackageHelper distractingPackageHelper;
     public StorageEventHelper storageEventHelper;
+    public Set<String> initialNonStoppedSystemPackages = new ArraySet<>();
+    public boolean shouldStopSystemPackagesByDefault;
 }
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index 830b096c..2538871 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -31,6 +31,7 @@
 import static com.android.server.pm.PackageManagerService.SCAN_AS_OEM;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_PRIVILEGED;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_PRODUCT;
+import static com.android.server.pm.PackageManagerService.SCAN_AS_STOPPED_SYSTEM_APP;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM_EXT;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_VENDOR;
@@ -198,6 +199,7 @@
         if (createNewPackage) {
             final boolean instantApp = (scanFlags & SCAN_AS_INSTANT_APP) != 0;
             final boolean virtualPreload = (scanFlags & SCAN_AS_VIRTUAL_PRELOAD) != 0;
+            final boolean isStoppedSystemApp = (scanFlags & SCAN_AS_STOPPED_SYSTEM_APP) != 0;
 
             // Flags contain system values stored in the server variant of AndroidPackage,
             // and so the server-side PackageInfoUtils is still called, even without a
@@ -212,7 +214,7 @@
                     AndroidPackageUtils.getRawPrimaryCpuAbi(parsedPackage),
                     AndroidPackageUtils.getRawSecondaryCpuAbi(parsedPackage),
                     parsedPackage.getLongVersionCode(), pkgFlags, pkgPrivateFlags, user,
-                    true /*allowInstall*/, instantApp, virtualPreload,
+                    true /*allowInstall*/, instantApp, virtualPreload, isStoppedSystemApp,
                     UserManagerService.getInstance(), usesSdkLibraries,
                     parsedPackage.getUsesSdkLibrariesVersionsMajor(), usesStaticLibraries,
                     parsedPackage.getUsesStaticLibrariesVersions(), parsedPackage.getMimeGroups(),
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 165e476..4f0a115 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -1041,7 +1041,7 @@
             File codePath, String legacyNativeLibraryPath, String primaryCpuAbi,
             String secondaryCpuAbi, long versionCode, int pkgFlags, int pkgPrivateFlags,
             UserHandle installUser, boolean allowInstall, boolean instantApp,
-            boolean virtualPreload, UserManagerService userManager,
+            boolean virtualPreload, boolean isStoppedSystemApp, UserManagerService userManager,
             String[] usesSdkLibraries, long[] usesSdkLibrariesVersions,
             String[] usesStaticLibraries, long[] usesStaticLibrariesVersions,
             Set<String> mimeGroupNames, @NonNull UUID domainSetId) {
@@ -1068,6 +1068,9 @@
             pkgSetting.setFlags(pkgFlags)
                     .setPrivateFlags(pkgPrivateFlags);
         } else {
+            int installUserId = installUser != null ? installUser.getIdentifier()
+                    : UserHandle.USER_SYSTEM;
+
             pkgSetting = new PackageSetting(pkgName, realPkgName, codePath,
                     legacyNativeLibraryPath, primaryCpuAbi, secondaryCpuAbi,
                     null /*cpuAbiOverrideString*/, versionCode, pkgFlags, pkgPrivateFlags,
@@ -1086,8 +1089,6 @@
                     Slog.i(PackageManagerService.TAG, "Stopping package " + pkgName, e);
                 }
                 List<UserInfo> users = getAllUsers(userManager);
-                int installUserId = installUser != null ? installUser.getIdentifier()
-                        : UserHandle.USER_SYSTEM;
                 if (users != null && allowInstall) {
                     for (UserInfo user : users) {
                         // By default we consider this app to be installed
@@ -1126,6 +1127,13 @@
                         );
                     }
                 }
+            } else if (isStoppedSystemApp) {
+                if (DEBUG_STOPPED) {
+                    RuntimeException e = new RuntimeException("here");
+                    e.fillInStackTrace();
+                    Slog.i(PackageManagerService.TAG, "Stopping system package " + pkgName, e);
+                }
+                pkgSetting.setStopped(true, installUserId);
             }
             if (sharedUser != null) {
                 pkgSetting.setAppId(sharedUser.mAppId);
@@ -4422,6 +4430,15 @@
                                 ps.getPackageName()));
                 // Only system apps are initially installed.
                 ps.setInstalled(shouldReallyInstall, userHandle);
+
+                // Non-Apex system apps, that are not included in the allowlist in
+                // initialNonStoppedSystemPackages, should be marked as stopped by default.
+                final boolean shouldBeStopped = service.mShouldStopSystemPackagesByDefault
+                        && ps.isSystem()
+                        && !ps.isApex()
+                        && !service.mInitialNonStoppedSystemPackages.contains(ps.getPackageName());
+                ps.setStopped(shouldBeStopped, userHandle);
+
                 // If userTypeInstallablePackages is the *only* reason why we're not installing,
                 // then uninstallReason is USER_TYPE. If there's a different reason, or if we
                 // actually are installing, put UNKNOWN.
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index eb3be54..26a990c 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -52,12 +52,14 @@
     // TODO(b/248408342): Move keep annotation to the method referencing these fields reflectively.
     @Keep public static final int USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE = 1;
     @Keep public static final int USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE = 2;
+    @Keep public static final int USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE = 3;
     @Keep public static final int USER_ASSIGNMENT_RESULT_FAILURE = -1;
 
     private static final String PREFIX_USER_ASSIGNMENT_RESULT = "USER_ASSIGNMENT_RESULT_";
     @IntDef(flag = false, prefix = {PREFIX_USER_ASSIGNMENT_RESULT}, value = {
             USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE,
             USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE,
+            USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE,
             USER_ASSIGNMENT_RESULT_FAILURE
     })
     public @interface UserAssignmentResult {}
diff --git a/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java b/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java
index 5bdcbb9..50a1d90 100644
--- a/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java
+++ b/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.UserInfo;
+import android.content.res.Resources;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Process;
@@ -36,6 +37,7 @@
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 
+import com.android.internal.R;
 import com.android.internal.os.RoSystemProperties;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.LocalServices;
@@ -103,6 +105,9 @@
         pw.println();
         pw.println("  is-headless-system-user-mode [-v | --verbose]");
         pw.println("    Checks whether the device uses headless system user mode.");
+        pw.println("  is-visible-background-users-on-default-display-supported [-v | --verbose]");
+        pw.println("    Checks whether the device allows users to be start visible on background "
+                + "in the default display.");
         pw.println("    It returns the effective mode, even when using emulation");
         pw.println("    (to get the real mode as well, use -v or --verbose)");
         pw.println();
@@ -129,6 +134,8 @@
                     return runSetSystemUserModeEmulation();
                 case "is-headless-system-user-mode":
                     return runIsHeadlessSystemUserMode();
+                case "is-visible-background-users-on-default-display-supported":
+                    return runIsVisibleBackgroundUserOnDefaultDisplaySupported();
                 case "is-user-visible":
                     return runIsUserVisible();
                 default:
@@ -431,19 +438,47 @@
                     return -1;
             }
         }
-
-        boolean isHsum = mService.isHeadlessSystemUserMode();
+        boolean effective = mService.isHeadlessSystemUserMode();
         if (!verbose) {
             // NOTE: do not change output below, as it's used by ITestDevice
             // (it's ok to change the verbose option though)
-            pw.println(isHsum);
+            pw.println(effective);
         } else {
-            pw.printf("effective=%b real=%b\n", isHsum,
+            pw.printf("effective=%b real=%b\n", effective,
                     RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER);
         }
         return 0;
     }
 
+    private int runIsVisibleBackgroundUserOnDefaultDisplaySupported() {
+        PrintWriter pw = getOutPrintWriter();
+
+        boolean verbose = false;
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-v":
+                case "--verbose":
+                    verbose = true;
+                    break;
+                default:
+                    pw.println("Invalid option: " + opt);
+                    return -1;
+            }
+        }
+
+        boolean effective = UserManager.isVisibleBackgroundUsersOnDefaultDisplayEnabled();
+        if (!verbose) {
+            // NOTE: do not change output below, as it's used by ITestDevice
+            // (it's ok to change the verbose option though)
+            pw.println(effective);
+        } else {
+            pw.printf("effective=%b real=%b\n", effective, Resources.getSystem()
+                    .getBoolean(R.bool.config_multiuserVisibleBackgroundUsersOnDefaultDisplay));
+        }
+        return 0;
+    }
+
     /**
      * Gets the {@link UserManager} associated with the context of the given user.
      */
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 77c32ae..4cf8c09 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -121,6 +121,16 @@
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(1)
                 .setLabel(0)
+                .setIconBadge(com.android.internal.R.drawable.ic_clone_icon_badge)
+                .setBadgePlain(com.android.internal.R.drawable.ic_clone_badge)
+                // Clone doesn't use BadgeNoBackground, so just set to BadgePlain as a placeholder.
+                .setBadgeNoBackground(com.android.internal.R.drawable.ic_clone_badge)
+                .setBadgeLabels(
+                        com.android.internal.R.string.clone_profile_label_badge)
+                .setBadgeColors(
+                        com.android.internal.R.color.system_neutral2_800)
+                .setDarkThemeBadgeColors(
+                        com.android.internal.R.color.system_neutral2_900)
                 .setDefaultRestrictions(null)
                 .setDefaultCrossProfileIntentFilters(getDefaultCloneCrossProfileIntentFilter())
                 .setDefaultUserProperties(new UserProperties.Builder()
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index 0a181a4..fe8a500 100644
--- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -22,6 +22,7 @@
 import static android.view.Display.INVALID_DISPLAY;
 
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE;
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
 import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND;
@@ -163,8 +164,7 @@
 
     UserVisibilityMediator(Handler handler) {
         this(UserManager.isVisibleBackgroundUsersEnabled(),
-                // TODO(b/261538232): get visibleBackgroundUserOnDefaultDisplayAllowed from UM
-                /* visibleBackgroundUserOnDefaultDisplayAllowed= */ false, handler);
+                UserManager.isVisibleBackgroundUsersOnDefaultDisplayEnabled(), handler);
     }
 
     @VisibleForTesting
@@ -230,7 +230,8 @@
                 Slogf.d(TAG, "result of getUserVisibilityOnStartLocked(%s)",
                         userAssignmentResultToString(result));
             }
-            if (result == USER_ASSIGNMENT_RESULT_FAILURE) {
+            if (result == USER_ASSIGNMENT_RESULT_FAILURE
+                    || result == USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE) {
                 return result;
             }
 
@@ -316,11 +317,19 @@
         }
 
         boolean visibleBackground = userStartMode == USER_START_MODE_BACKGROUND_VISIBLE;
-        if (displayId == DEFAULT_DISPLAY && visibleBackground
-                && !mVisibleBackgroundUserOnDefaultDisplayAllowed
-                && !isProfile(userId, profileGroupId)) {
-            Slogf.wtf(TAG, "cannot start full user (%d) visible on default display", userId);
-            return USER_ASSIGNMENT_RESULT_FAILURE;
+        if (displayId == DEFAULT_DISPLAY && visibleBackground) {
+            if (mVisibleBackgroundUserOnDefaultDisplayAllowed && isCurrentUserLocked(userId)) {
+                // Shouldn't happen - UserController returns before calling this method
+                Slogf.wtf(TAG, "trying to start current user (%d) visible in background on default"
+                        + " display", userId);
+                return USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE;
+
+            }
+            if (!mVisibleBackgroundUserOnDefaultDisplayAllowed
+                    && !isProfile(userId, profileGroupId)) {
+                Slogf.wtf(TAG, "cannot start full user (%d) visible on default display", userId);
+                return USER_ASSIGNMENT_RESULT_FAILURE;
+            }
         }
 
         boolean foreground = userStartMode == USER_START_MODE_FOREGROUND;
@@ -406,7 +415,7 @@
                 // parent is the current user, as the current user is always assigned to the
                 // DEFAULT_DISPLAY).
                 if (DBG) {
-                    Slogf.d(TAG, "ignoring mapping for default display for user %d starting as %s",
+                    Slogf.d(TAG, "Ignoring mapping for default display for user %d starting as %s",
                             userId, userStartModeToString(userStartMode));
                 }
                 return SECONDARY_DISPLAY_MAPPING_NOT_NEEDED;
@@ -1007,6 +1016,15 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private boolean isCurrentUserLocked(@UserIdInt int userId) {
+        // Special case as NO_PROFILE_GROUP_ID == USER_NULL
+        if (userId == USER_NULL || mCurrentUserId == USER_NULL) {
+            return false;
+        }
+        return mCurrentUserId == userId;
+    }
+
     private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
         synchronized (mLock) {
             // Special case as NO_PROFILE_GROUP_ID == USER_NULL
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 287cc29..42a8cb1 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -51,7 +51,7 @@
 import android.content.pm.PermissionGroupInfo;
 import android.content.pm.PermissionInfo;
 import android.content.pm.permission.SplitPermissionInfoParcelable;
-import android.healthconnect.HealthConnectManager;
+import android.health.connect.HealthConnectManager;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.Process;
diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index 7f733ef..8d7f782 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -366,12 +366,12 @@
             }
             mListener = listener;
         }
-        notifySupportedStatesChanged();
+        notifySupportedStatesChanged(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED);
         notifyDeviceStateChangedIfNeeded();
     }
 
     /** Notifies the listener that the set of supported device states has changed. */
-    private void notifySupportedStatesChanged() {
+    private void notifySupportedStatesChanged(@SupportedStatesUpdatedReason int reason) {
         List<DeviceState> supportedStates = new ArrayList<>();
         Listener listener;
         synchronized (mLock) {
@@ -390,7 +390,7 @@
         }
 
         listener.onSupportedDeviceStatesChanged(
-                supportedStates.toArray(new DeviceState[supportedStates.size()]));
+                supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
     }
 
     /** Computes the current device state and notifies the listener of a change, if needed. */
@@ -688,7 +688,10 @@
         if (isThermalStatusCriticalOrAbove != isPreviousThermalStatusCriticalOrAbove) {
             Slog.i(TAG, "Updating supported device states due to thermal status change."
                     + " isThermalStatusCriticalOrAbove: " + isThermalStatusCriticalOrAbove);
-            notifySupportedStatesChanged();
+            notifySupportedStatesChanged(
+                    isThermalStatusCriticalOrAbove
+                            ? SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL
+                            : SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL);
         }
     }
 
diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp
index ec42324..db7fced 100644
--- a/services/core/jni/com_android_server_display_DisplayControl.cpp
+++ b/services/core/jni/com_android_server_display_DisplayControl.cpp
@@ -23,9 +23,11 @@
 
 namespace android {
 
-static jobject nativeCreateDisplay(JNIEnv* env, jclass clazz, jstring nameObj, jboolean secure) {
+static jobject nativeCreateDisplay(JNIEnv* env, jclass clazz, jstring nameObj, jboolean secure,
+                                   jfloat requestedRefreshRate) {
     ScopedUtfChars name(env, nameObj);
-    sp<IBinder> token(SurfaceComposerClient::createDisplay(String8(name.c_str()), bool(secure)));
+    sp<IBinder> token(SurfaceComposerClient::createDisplay(String8(name.c_str()), bool(secure),
+                                                           requestedRefreshRate));
     return javaObjectForIBinder(env, token);
 }
 
@@ -134,7 +136,7 @@
 
 static const JNINativeMethod sDisplayMethods[] = {
         // clang-format off
-    {"nativeCreateDisplay", "(Ljava/lang/String;Z)Landroid/os/IBinder;",
+    {"nativeCreateDisplay", "(Ljava/lang/String;ZF)Landroid/os/IBinder;",
             (void*)nativeCreateDisplay },
     {"nativeDestroyDisplay", "(Landroid/os/IBinder;)V",
             (void*)nativeDestroyDisplay },
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 75484d1..a39e021 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -1129,6 +1129,7 @@
                 false /*allowInstall*/,
                 false /*instantApp*/,
                 false /*virtualPreload*/,
+                false /* stopped */,
                 UserManagerService.getInstance(),
                 null /*usesSdkLibraries*/,
                 null /*usesSdkLibrariesVersions*/,
@@ -1173,6 +1174,7 @@
                 true /*allowInstall*/,
                 false /*instantApp*/,
                 false /*virtualPreload*/,
+                false /* stopped */,
                 UserManagerService.getInstance(),
                 null /*usesSdkLibraries*/,
                 null /*usesSdkLibrariesVersions*/,
@@ -1217,6 +1219,7 @@
                 false /*allowInstall*/,
                 false /*instantApp*/,
                 false /*virtualPreload*/,
+                false /* stopped */,
                 UserManagerService.getInstance(),
                 null /*usesSdkLibraries*/,
                 null /*usesSdkLibrariesVersions*/,
@@ -1262,6 +1265,7 @@
                 false /*allowInstall*/,
                 false /*instantApp*/,
                 false /*virtualPreload*/,
+                false /* stopped */,
                 UserManagerService.getInstance(),
                 null /*usesSdkLibraries*/,
                 null /*usesSdkLibrariesVersions*/,
@@ -1284,6 +1288,48 @@
         verifyUserState(userState, false /*notLaunched*/, false /*stopped*/, true /*installed*/);
     }
 
+    /** Create a new stopped system PackageSetting */
+    @Test
+    public void testCreateNewSetting05() {
+        final PackageSetting testPkgSetting01 = Settings.createNewSetting(
+                PACKAGE_NAME,
+                null /*originalPkg*/,
+                null /*disabledPkg*/,
+                null /*realPkgName*/,
+                null /*sharedUser*/,
+                UPDATED_CODE_PATH /*codePath*/,
+                null /*legacyNativeLibraryPath*/,
+                "arm64-v8a" /*primaryCpuAbi*/,
+                "armeabi" /*secondaryCpuAbi*/,
+                UPDATED_VERSION_CODE /*versionCode*/,
+                ApplicationInfo.FLAG_SYSTEM /*pkgFlags*/,
+                0 /*pkgPrivateFlags*/,
+                UserHandle.SYSTEM /*installUser*/,
+                false /*allowInstall*/,
+                false /*instantApp*/,
+                false /*virtualPreload*/,
+                true /* stopped */,
+                UserManagerService.getInstance(),
+                null /*usesSdkLibraries*/,
+                null /*usesSdkLibrariesVersions*/,
+                null /*usesStaticLibraries*/,
+                null /*usesStaticLibrariesVersions*/,
+                null /*mimeGroups*/,
+                UUID.randomUUID());
+        assertThat(testPkgSetting01.getAppId(), is(0));
+        assertThat(testPkgSetting01.getPath(), is(UPDATED_CODE_PATH));
+        assertThat(testPkgSetting01.getPackageName(), is(PACKAGE_NAME));
+        assertThat(testPkgSetting01.getFlags(), is(ApplicationInfo.FLAG_SYSTEM));
+        assertThat(testPkgSetting01.getPrivateFlags(), is(0));
+        assertThat(testPkgSetting01.getPrimaryCpuAbi(), is("arm64-v8a"));
+        assertThat(testPkgSetting01.getPrimaryCpuAbiLegacy(), is("arm64-v8a"));
+        assertThat(testPkgSetting01.getSecondaryCpuAbi(), is("armeabi"));
+        assertThat(testPkgSetting01.getSecondaryCpuAbiLegacy(), is("armeabi"));
+        assertThat(testPkgSetting01.getVersionCode(), is(UPDATED_VERSION_CODE));
+        final PackageUserState userState = testPkgSetting01.readUserState(0);
+        verifyUserState(userState, false /*notLaunched*/, true /*stopped*/, true /*installed*/);
+    }
+
     @Test
     public void testSetPkgStateLibraryFiles_addNewFiles() {
         final PackageSetting packageSetting = createPackageSetting("com.foo");
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index ef470fe..3b20735 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -112,7 +112,6 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
 
 import android.Manifest;
 import android.app.ActivityManager;
@@ -1265,7 +1264,30 @@
         mService.interactiveStateChangedLocked(false);
         mService.interactiveStateChangedLocked(true);
         runnableCaptor.getValue().run();
-        verify(mMockContext).sendBroadcastAsUser(mService.mTimeTickIntent, UserHandle.ALL);
+        final ArgumentCaptor<Bundle> optionsCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mMockContext).sendBroadcastAsUser(eq(mService.mTimeTickIntent), eq(UserHandle.ALL),
+                isNull(), optionsCaptor.capture());
+        verifyTimeTickBroadcastOptions(optionsCaptor.getValue());
+    }
+
+    @Test
+    public void sendsTimeTickOnAlarmTrigger() throws Exception {
+        final ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        // Stubbing so the handler doesn't actually run the runnable.
+        doReturn(true).when(mService.mHandler).post(runnableCaptor.capture());
+        mService.mTimeTickTrigger.doAlarm(mock(IAlarmCompleteListener.class));
+        runnableCaptor.getValue().run();
+        final ArgumentCaptor<Bundle> optionsCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mMockContext).sendBroadcastAsUser(eq(mService.mTimeTickIntent), eq(UserHandle.ALL),
+                isNull(), optionsCaptor.capture());
+        verifyTimeTickBroadcastOptions(optionsCaptor.getValue());
+    }
+
+    private void verifyTimeTickBroadcastOptions(Bundle actualOptionsBundle) {
+        final BroadcastOptions actualOptions = new BroadcastOptions(actualOptionsBundle);
+        assertEquals(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT,
+                actualOptions.getDeliveryGroupPolicy());
+        assertTrue(actualOptions.isDeferUntilActive());
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index c40017a..2fb6c23 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -110,6 +110,7 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -225,6 +226,31 @@
         }
     }
 
+    /**
+     * Replace the process LRU with the given processes.
+     * @param apps
+     */
+    private void setProcessesToLru(ProcessRecord... apps) {
+        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
+        lru.clear();
+        Collections.addAll(lru, apps);
+    }
+
+    /**
+     * Run updateOomAdjLocked().
+     * - If there's only one process, then it calls updateOomAdjLocked(ProcessRecord, int).
+     * - Otherwise, sets the processes to the LRU and run updateOomAdjLocked(int).
+     */
+    private void updateOomAdj(ProcessRecord... apps) {
+        if (apps.length == 1) {
+            sService.mOomAdjuster.updateOomAdjLocked(apps[0], OomAdjuster.OOM_ADJ_REASON_NONE);
+        } else {
+            setProcessesToLru(apps);
+            sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+            sService.mProcessList.getLruProcessesLOSP().clear();
+        }
+    }
+
     @SuppressWarnings("GuardedBy")
     @Test
     public void testUpdateOomAdj_DoOne_Persistent_TopUi_Sleeping() {
@@ -233,7 +259,7 @@
         app.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
         app.mState.setHasTopUi(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_ASLEEP);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
 
         assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERSISTENT_PROC_ADJ,
@@ -248,7 +274,7 @@
         app.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
         app.mState.setHasTopUi(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_PERSISTENT_UI, PERSISTENT_PROC_ADJ,
                 SCHED_GROUP_TOP_APP);
@@ -262,7 +288,7 @@
         app.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
         doReturn(app).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doReturn(null).when(sService).getTopApp();
 
         assertProcStates(app, PROCESS_STATE_PERSISTENT_UI, PERSISTENT_PROC_ADJ,
@@ -277,7 +303,7 @@
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
         doReturn(app).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doReturn(null).when(sService).getTopApp();
 
         assertProcStates(app, PROCESS_STATE_TOP, FOREGROUND_APP_ADJ, SCHED_GROUP_TOP_APP);
@@ -291,7 +317,7 @@
         doReturn(PROCESS_STATE_TOP_SLEEPING).when(sService.mAtmInternal).getTopProcessState();
         app.mState.setRunningRemoteAnimation(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
 
         assertProcStates(app, PROCESS_STATE_TOP_SLEEPING, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
@@ -304,7 +330,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
         doReturn(mock(ActiveInstrumentation.class)).when(app).getActiveInstrumentation();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doCallRealMethod().when(app).getActiveInstrumentation();
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, FOREGROUND_APP_ADJ,
@@ -319,7 +345,7 @@
         doReturn(true).when(sService).isReceivingBroadcastLocked(any(ProcessRecord.class),
                 any(int[].class));
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doReturn(false).when(sService).isReceivingBroadcastLocked(any(ProcessRecord.class),
                 any(int[].class));
 
@@ -333,7 +359,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
         app.mServices.startExecutingService(mock(ServiceRecord.class));
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_SERVICE, FOREGROUND_APP_ADJ, SCHED_GROUP_BACKGROUND);
     }
@@ -346,7 +372,7 @@
         doReturn(PROCESS_STATE_TOP_SLEEPING).when(sService.mAtmInternal).getTopProcessState();
         doReturn(app).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_ASLEEP);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doReturn(null).when(sService).getTopApp();
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
@@ -363,7 +389,7 @@
         app.mState.setCurRawAdj(CACHED_APP_MIN_ADJ);
         doReturn(null).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, CACHED_APP_MIN_ADJ,
                 SCHED_GROUP_BACKGROUND);
@@ -389,7 +415,7 @@
         })).when(wpc).computeOomAdjFromActivities(
                 any(WindowProcessController.ComputeOomAdjCallback.class));
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
     }
@@ -403,7 +429,7 @@
         doReturn(true).when(wpc).hasRecentTasks();
         app.mState.setLastTopTime(SystemClock.uptimeMillis());
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doCallRealMethod().when(wpc).hasRecentTasks();
 
         assertEquals(PROCESS_STATE_CACHED_RECENT, app.mState.getSetProcState());
@@ -417,7 +443,7 @@
         app.mServices.setHasForegroundServices(true, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION,
                 /* hasNoneType=*/false);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -430,7 +456,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
         app.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -458,7 +484,7 @@
             app.mState.setLastTopTime(SystemClock.uptimeMillis());
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
 
-            sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+            updateOomAdj(app);
 
             assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
                     PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 1, SCHED_GROUP_DEFAULT);
@@ -477,7 +503,7 @@
                     - sService.mConstants.TOP_TO_FGS_GRACE_DURATION);
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
 
-            sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+            updateOomAdj(app);
 
             assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
                     PERCEPTIBLE_MEDIUM_APP_ADJ + 1, SCHED_GROUP_DEFAULT);
@@ -503,7 +529,7 @@
                     - sService.mConstants.TOP_TO_FGS_GRACE_DURATION);
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
 
-            sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+            updateOomAdj(app);
 
             // Procstate should be lower than FGS. (It should be SERVICE)
             assertEquals(app.mState.getSetProcState(), PROCESS_STATE_SERVICE);
@@ -520,7 +546,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
         app.mState.setHasOverlayUi(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -534,7 +560,7 @@
         app.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         app.mState.setLastTopTime(SystemClock.uptimeMillis());
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE,
                 PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ, SCHED_GROUP_DEFAULT);
@@ -558,7 +584,7 @@
             s.getConnections().clear();
             app.mServices.updateHasTopStartedAlmostPerceptibleServices();
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-            sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+            updateOomAdj(app);
 
             assertEquals(PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 2, app.mState.getSetAdj());
         }
@@ -578,7 +604,7 @@
                     nowUptime - 2 * sService.mConstants.mServiceBindAlmostPerceptibleTimeoutMs;
             app.mServices.updateHasTopStartedAlmostPerceptibleServices();
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-            sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+            updateOomAdj(app);
 
             assertEquals(PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 2, app.mState.getSetAdj());
         }
@@ -599,7 +625,7 @@
             s.getConnections().clear();
             app.mServices.updateHasTopStartedAlmostPerceptibleServices();
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-            sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+            updateOomAdj(app);
 
             assertNotEquals(PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ + 2, app.mState.getSetAdj());
         }
@@ -611,7 +637,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
         app.mState.setForcingToImportant(new Object());
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -625,7 +651,7 @@
         WindowProcessController wpc = app.getWindowProcessController();
         doReturn(true).when(wpc).isHeavyWeightProcess();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doReturn(false).when(wpc).isHeavyWeightProcess();
 
         assertProcStates(app, PROCESS_STATE_HEAVY_WEIGHT, HEAVY_WEIGHT_APP_ADJ,
@@ -640,7 +666,7 @@
         WindowProcessController wpc = app.getWindowProcessController();
         doReturn(true).when(wpc).isHomeProcess();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_HOME, HOME_APP_ADJ, SCHED_GROUP_BACKGROUND);
     }
@@ -654,7 +680,7 @@
         doReturn(true).when(wpc).isPreviousProcess();
         doReturn(true).when(wpc).hasActivities();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_LAST_ACTIVITY, PREVIOUS_APP_ADJ,
                 SCHED_GROUP_BACKGROUND);
@@ -669,7 +695,7 @@
         backupTarget.app = app;
         doReturn(backupTarget).when(sService.mBackupTargets).get(anyInt());
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         doReturn(null).when(sService.mBackupTargets).get(anyInt());
 
         assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, BACKUP_APP_ADJ,
@@ -683,7 +709,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
         app.mServices.setHasClientActivities(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertEquals(PROCESS_STATE_CACHED_ACTIVITY_CLIENT, app.mState.getSetProcState());
     }
@@ -695,7 +721,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
         app.mServices.setTreatLikeActivity(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertEquals(PROCESS_STATE_CACHED_ACTIVITY, app.mState.getSetProcState());
     }
@@ -712,7 +738,7 @@
         s.lastActivity = SystemClock.uptimeMillis();
         app.mServices.startService(s);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_B_ADJ, SCHED_GROUP_BACKGROUND);
     }
@@ -724,7 +750,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
         app.mState.setMaxAdj(PERCEPTIBLE_LOW_APP_ADJ);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, PERCEPTIBLE_LOW_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -739,7 +765,7 @@
         app.mState.setCurRawAdj(SERVICE_ADJ);
         doReturn(null).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertTrue(ProcessList.CACHED_APP_MIN_ADJ <= app.mState.getSetAdj());
         assertTrue(ProcessList.CACHED_APP_MAX_ADJ >= app.mState.getSetAdj());
@@ -756,7 +782,7 @@
         s.lastActivity = SystemClock.uptimeMillis();
         app.mServices.startService(s);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND);
     }
@@ -774,7 +800,7 @@
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
         doReturn(client).when(sService).getTopApp();
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
         doReturn(null).when(sService).getTopApp();
 
         assertProcStates(app, PROCESS_STATE_SERVICE, FIRST_CACHED_ADJ, SCHED_GROUP_BACKGROUND);
@@ -790,7 +816,7 @@
         bindService(app, client, null, Context.BIND_WAIVE_PRIORITY
                 | Context.BIND_TREAT_LIKE_ACTIVITY, mock(IBinder.class));
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(PROCESS_STATE_CACHED_ACTIVITY, app.mState.getSetProcState());
     }
@@ -810,7 +836,7 @@
                 mock(ActivityServiceConnectionsHolder.class));
         doReturn(true).when(cr.activity).isActivityVisible();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
         assertEquals(SCHED_GROUP_TOP_APP_BOUND, app.mState.getSetSchedGroup());
@@ -822,11 +848,8 @@
         ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
         bindService(app, app, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, FIRST_CACHED_ADJ, SCHED_GROUP_BACKGROUND);
     }
@@ -841,7 +864,7 @@
         client.mServices.setTreatLikeActivity(true);
         bindService(app, client, null, 0, mock(IBinder.class));
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(PROCESS_STATE_CACHED_EMPTY, app.mState.getSetProcState());
     }
@@ -861,7 +884,7 @@
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
         doReturn(client).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
         doReturn(null).when(sService).getTopApp();
 
         assertEquals(PREVIOUS_APP_ADJ, app.mState.getSetAdj());
@@ -878,7 +901,7 @@
         client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
         client.mState.setHasTopUi(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -894,7 +917,7 @@
         bindService(app, client, null, Context.BIND_IMPORTANT, mock(IBinder.class));
         client.mServices.startExecutingService(mock(ServiceRecord.class));
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
     }
@@ -910,7 +933,7 @@
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
         doReturn(client).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
         doReturn(null).when(sService).getTopApp();
 
         assertProcStates(app, PROCESS_STATE_BOUND_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_DEFAULT);
@@ -926,9 +949,10 @@
         bindService(app, client, null, Context.BIND_FOREGROUND_SERVICE, mock(IBinder.class));
         client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(PROCESS_STATE_BOUND_FOREGROUND_SERVICE, app.mState.getSetProcState());
+        assertEquals(PROCESS_STATE_PERSISTENT, client.mState.getSetProcState());
     }
 
     @SuppressWarnings("GuardedBy")
@@ -941,7 +965,7 @@
         bindService(app, client, null, Context.BIND_NOT_FOREGROUND, mock(IBinder.class));
         client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertEquals(PROCESS_STATE_TRANSIENT_BACKGROUND, app.mState.getSetProcState());
     }
@@ -956,7 +980,7 @@
         bindService(app, client, null, 0, mock(IBinder.class));
         client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(PROCESS_STATE_FOREGROUND_SERVICE, app.mState.getSetProcState());
     }
@@ -973,13 +997,14 @@
         backupTarget.app = client;
         doReturn(backupTarget).when(sService.mBackupTargets).get(anyInt());
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
+
         doReturn(null).when(sService.mBackupTargets).get(anyInt());
 
         assertEquals(BACKUP_APP_ADJ, app.mState.getSetAdj());
 
         client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertEquals(PERSISTENT_SERVICE_ADJ, app.mState.getSetAdj());
     }
@@ -994,7 +1019,7 @@
         bindService(app, client, null, Context.BIND_NOT_PERCEPTIBLE, mock(IBinder.class));
         client.mState.setRunningRemoteAnimation(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertEquals(PERCEPTIBLE_LOW_APP_ADJ, app.mState.getSetAdj());
     }
@@ -1009,7 +1034,7 @@
         bindService(app, client, null, Context.BIND_NOT_VISIBLE, mock(IBinder.class));
         client.mState.setRunningRemoteAnimation(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(PERCEPTIBLE_APP_ADJ, app.mState.getSetAdj());
     }
@@ -1024,7 +1049,7 @@
         bindService(app, client, null, 0, mock(IBinder.class));
         client.mState.setHasOverlayUi(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(PERCEPTIBLE_APP_ADJ, app.mState.getSetAdj());
     }
@@ -1040,7 +1065,7 @@
             bindService(app, client, null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class));
             client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-            sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+            updateOomAdj(client, app);
 
             assertEquals(PERCEPTIBLE_MEDIUM_APP_ADJ + 2, app.mState.getSetAdj());
         }
@@ -1055,7 +1080,7 @@
             bindService(app, client, null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class));
             client.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
             sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-            sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+            updateOomAdj(client, app);
             doReturn(false).when(wpc).isHeavyWeightProcess();
 
             assertEquals(PERCEPTIBLE_MEDIUM_APP_ADJ + 2, app.mState.getSetAdj());
@@ -1072,7 +1097,7 @@
         bindService(app, client, null, 0, mock(IBinder.class));
         client.mState.setRunningRemoteAnimation(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(VISIBLE_APP_ADJ, app.mState.getSetAdj());
     }
@@ -1087,7 +1112,7 @@
         bindService(app, client, null, Context.BIND_IMPORTANT_BACKGROUND, mock(IBinder.class));
         client.mState.setHasOverlayUi(true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertEquals(PROCESS_STATE_IMPORTANT_BACKGROUND, app.mState.getSetProcState());
     }
@@ -1098,7 +1123,7 @@
         ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
         bindProvider(app, app, null, null, false);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, FIRST_CACHED_ADJ, SCHED_GROUP_BACKGROUND);
     }
@@ -1112,12 +1137,8 @@
                 MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
         bindProvider(app, client, null, null, false);
         client.mServices.setTreatLikeActivity(true);
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client);
 
         assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, FIRST_CACHED_ADJ, SCHED_GROUP_BACKGROUND);
     }
@@ -1133,7 +1154,7 @@
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
         doReturn(client).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
         doReturn(null).when(sService).getTopApp();
 
         assertProcStates(app, PROCESS_STATE_BOUND_TOP, FOREGROUND_APP_ADJ, SCHED_GROUP_DEFAULT);
@@ -1149,7 +1170,7 @@
         client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindProvider(app, client, null, null, false);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1164,7 +1185,7 @@
                 MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
         bindProvider(app, client, null, null, true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, app);
 
         assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND, FOREGROUND_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1177,7 +1198,7 @@
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
         app.mProviders.setLastProviderTime(SystemClock.uptimeMillis());
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_LAST_ACTIVITY, PREVIOUS_APP_ADJ,
                 SCHED_GROUP_BACKGROUND);
@@ -1197,7 +1218,7 @@
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
         doReturn(client2).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, client2, app);
         doReturn(null).when(sService).getTopApp();
 
         assertProcStates(app, PROCESS_STATE_BOUND_TOP, VISIBLE_APP_ADJ,
@@ -1217,7 +1238,7 @@
         bindService(app, client2, null, 0, mock(IBinder.class));
         client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, client2, app);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1236,7 +1257,7 @@
         bindService(client, client2, null, 0, mock(IBinder.class));
         client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, client2, app);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1255,13 +1276,12 @@
         bindService(client, client2, null, 0, mock(IBinder.class));
         client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindService(client2, app, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
+
+        // Note: We add processes to LRU but still call updateOomAdjLocked() with a specific
+        // processes.
+        setProcessesToLru(app, client, client2);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1292,13 +1312,8 @@
                 MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
         bindService(client2, client, null, 0, mock(IBinder.class));
         client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1321,13 +1336,8 @@
         bindService(client, client2, null, 0, mock(IBinder.class));
         bindService(client2, client, null, 0, mock(IBinder.class));
         client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1357,15 +1367,8 @@
         bindService(client3, client4, null, 0, mock(IBinder.class));
         bindService(client4, client3, null, 0, mock(IBinder.class));
         client.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
-        lru.add(client3);
-        lru.add(client4);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2, client3, client4);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1396,14 +1399,8 @@
                 MOCKAPP4_PROCESSNAME, MOCKAPP4_PACKAGENAME, false));
         client3.mState.setForcingToImportant(new Object());
         bindService(app, client3, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
-        lru.add(client3);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2, client3);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1427,14 +1424,8 @@
                 MOCKAPP4_PROCESSNAME, MOCKAPP4_PACKAGENAME, false));
         client3.mState.setForcingToImportant(new Object());
         bindService(app, client3, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
-        lru.add(client3);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2, client3);
 
         assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1460,15 +1451,8 @@
                 MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
         client4.mState.setForcingToImportant(new Object());
         bindService(app, client4, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
-        lru.add(client3);
-        lru.add(client4);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2, client3, client4);
 
         assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1496,15 +1480,8 @@
                 MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
         client4.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindService(app, client4, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
-        lru.add(client3);
-        lru.add(client4);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2, client3, client4);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1529,7 +1506,7 @@
         client3.mState.setForcingToImportant(new Object());
         bindService(app, client3, null, 0, mock(IBinder.class));
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, client2, client3, app);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1548,7 +1525,7 @@
         bindProvider(client, client2, null, null, false);
         client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, client2, app);
 
         assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1567,13 +1544,8 @@
         bindProvider(client, client2, null, null, false);
         client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindService(client2, app, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2);
 
         assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1592,7 +1564,7 @@
         bindProvider(client, client2, null, null, false);
         client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client, client2, app);
 
         assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1611,14 +1583,8 @@
         bindProvider(client, client2, null, null, false);
         client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindProvider(client2, app, null, null, false);
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
-
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2);
 
         assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1643,8 +1609,7 @@
         client2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
 
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
-        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client1, client2, app1, app2);
 
         assertProcStates(app1, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1653,8 +1618,7 @@
 
         bindService(app1, client1, null, Context.BIND_SCHEDULE_LIKE_TOP_APP, mock(IBinder.class));
         bindService(app2, client2, null, Context.BIND_SCHEDULE_LIKE_TOP_APP, mock(IBinder.class));
-        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
-        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app1, app2);
 
         assertProcStates(app1, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
                 SCHED_GROUP_TOP_APP);
@@ -1662,8 +1626,7 @@
                 SCHED_GROUP_DEFAULT);
 
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_ASLEEP);
-        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
-        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client1, client2, app1, app2);
         assertProcStates(app1, PROCESS_STATE_IMPORTANT_FOREGROUND, VISIBLE_APP_ADJ,
                 SCHED_GROUP_TOP_APP);
         assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
@@ -1689,8 +1652,7 @@
         final ServiceRecord s2 = bindService(app2, client2, null,
                 Context.BIND_IMPORTANT, mock(IBinder.class));
 
-        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
-        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client1, client2, app1, app2);
 
         assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1699,7 +1661,7 @@
 
         bindService(app2, client1, s2, Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE,
                 mock(IBinder.class));
-        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app2);
         assertProcStates(app2, PROCESS_STATE_PERSISTENT, PERSISTENT_SERVICE_ADJ,
                 SCHED_GROUP_DEFAULT);
 
@@ -1715,10 +1677,10 @@
         bindService(app2, client2, s2, Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE,
                 mock(IBinder.class));
 
-        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
-        sService.mOomAdjuster.updateOomAdjLocked(app2, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app1, app2);
 
-        assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+        // VISIBLE_APP_ADJ is the max oom-adj for BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE.
+        assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, VISIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
         assertProcStates(app2, PROCESS_STATE_IMPORTANT_FOREGROUND, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1747,7 +1709,7 @@
 
         bindService(app1, client1, null, Context.BIND_NOT_PERCEPTIBLE, mock(IBinder.class));
 
-        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client1, app1);
 
         assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1767,7 +1729,7 @@
 
         bindService(app1, client1, null, Context.BIND_ALMOST_PERCEPTIBLE, mock(IBinder.class));
 
-        sService.mOomAdjuster.updateOomAdjLocked(app1, OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(client1, app1);
 
         assertProcStates(app1, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1798,13 +1760,7 @@
 
         client1.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         client2.mState.setForcingToImportant(new Object());
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app1);
-        lru.add(app2);
-        lru.add(app3);
-        lru.add(client1);
-        lru.add(client2);
+        setProcessesToLru(app1, app2, app3, client1, client2);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
 
         final ComponentName cn1 = ComponentName.unflattenFromString(
@@ -1891,13 +1847,9 @@
         ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
                 MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
         app2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(app2);
+
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
-        lru.clear();
+        updateOomAdj(app, app2);
 
         assertProcStates(app, PROCESS_STATE_TRANSIENT_BACKGROUND, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1915,13 +1867,8 @@
                 MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
         app2.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindService(app, app2, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(app2);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
-        lru.clear();
+        updateOomAdj(app, app2);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1942,14 +1889,8 @@
         bindService(app2, app3, null, 0, mock(IBinder.class));
         app3.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindService(app3, app, null, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(app2);
-        lru.add(app3);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
-        lru.clear();
+        updateOomAdj(app, app2, app3);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -1990,16 +1931,8 @@
                 MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
         app5.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindService(app, app5, s, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(app2);
-        lru.add(app3);
-        lru.add(app4);
-        lru.add(app5);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
-        lru.clear();
+        updateOomAdj(app, app2, app3, app4, app5);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -2035,16 +1968,8 @@
                 MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
         app5.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindService(app, app5, s, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app5);
-        lru.add(app4);
-        lru.add(app3);
-        lru.add(app2);
-        lru.add(app);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
-        lru.clear();
+        updateOomAdj(app5, app4, app3, app2, app);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -2080,16 +2005,8 @@
                 MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
         app5.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindService(app, app5, s, 0, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app3);
-        lru.add(app4);
-        lru.add(app2);
-        lru.add(app);
-        lru.add(app5);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
-        lru.clear();
+        updateOomAdj(app3, app4, app2, app, app5);
 
         assertProcStates(app, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -2119,18 +2036,13 @@
                 MOCKAPP4_PROCESSNAME, MOCKAPP4_PACKAGENAME, false));
         client3.mState.setMaxAdj(PERSISTENT_PROC_ADJ);
         bindService(app, client3, null, Context.BIND_INCLUDE_CAPABILITIES, mock(IBinder.class));
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(client);
-        lru.add(client2);
-        lru.add(client3);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
+        updateOomAdj(app, client, client2, client3);
 
-        assertEquals(PROCESS_CAPABILITY_ALL, client.mState.getSetCapability());
-        assertEquals(PROCESS_CAPABILITY_ALL, client2.mState.getSetCapability());
-        assertEquals(PROCESS_CAPABILITY_ALL, app.mState.getSetCapability());
+        final int expected = PROCESS_CAPABILITY_ALL;
+        assertEquals(expected, client.mState.getSetCapability());
+        assertEquals(expected, client2.mState.getSetCapability());
+        assertEquals(expected, app.mState.getSetCapability());
     }
 
     @SuppressWarnings("GuardedBy")
@@ -2155,16 +2067,8 @@
                 MOCKAPP5_PROCESSNAME, MOCKAPP5_PACKAGENAME, false));
         app5.mServices.setHasForegroundServices(true, 0, /* hasNoneType=*/true);
         bindProvider(app, app5, cr, null, false);
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app);
-        lru.add(app2);
-        lru.add(app3);
-        lru.add(app4);
-        lru.add(app5);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
-        lru.clear();
+        updateOomAdj(app, app2, app3, app4, app5);
 
         assertProcStates(app, PROCESS_STATE_BOUND_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
@@ -2202,15 +2106,9 @@
         doCallRealMethod().when(s).getConnections();
         s.startRequested = true;
         s.lastActivity = now;
-        ArrayList<ProcessRecord> lru = sService.mProcessList.getLruProcessesLOSP();
-        lru.clear();
-        lru.add(app3);
-        lru.add(app2);
-        lru.add(app);
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
         sService.mOomAdjuster.mNumServiceProcs = 3;
-        sService.mOomAdjuster.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_NONE);
-        lru.clear();
+        updateOomAdj(app3, app2, app);
 
         assertEquals(SERVICE_B_ADJ, app3.mState.getSetAdj());
         assertEquals(SERVICE_ADJ, app2.mState.getSetAdj());
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUPANDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUPANDTest.java
index f69054b..8979585 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUPANDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUPANDTest.java
@@ -20,6 +20,7 @@
 import static android.view.Display.INVALID_DISPLAY;
 
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE;
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
 import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
 import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
@@ -129,7 +130,28 @@
     }
 
     @Test
-    public void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedVisibleOnBg()
+    public void
+       testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedVisibleOnBgOnSecondaryDisplay()
+            throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID));
+        startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
+        expectNoDisplayAssignedToUser(PROFILE_USER_ID);
+        expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID);
+
+        assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+
+        listener.verify();
+    }
+
+    @Test
+    public void
+        testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedVisibleOnBgOnDefaultDisplay()
             throws Exception {
         AsyncUserVisibilityListener listener = addListenerForEvents(
                 onVisible(PARENT_USER_ID),
@@ -183,4 +205,25 @@
         assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
         listener.verify();
     }
+
+    @Test
+    public void testStartVisibleBgUser_onDefaultDisplay_currentUserId() throws Exception {
+        int currentUserId = INITIAL_CURRENT_USER_ID;
+
+        AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
+        int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId,
+                BG_VISIBLE, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_ALREADY_VISIBLE);
+
+        // Assert current user visibility
+        expectUserIsVisible(currentUserId);
+        expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
+        expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
+
+        assertUserCanBeAssignedExtraDisplay(currentUserId, OTHER_SECONDARY_DISPLAY_ID);
+
+        listener.verify();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index 82f6493..c952609 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -945,13 +945,15 @@
             }
 
             mListener = listener;
-            mListener.onSupportedDeviceStatesChanged(mSupportedDeviceStates);
+            mListener.onSupportedDeviceStatesChanged(mSupportedDeviceStates,
+                    SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED);
             mListener.onStateChanged(mSupportedDeviceStates[0].getIdentifier());
         }
 
         public void notifySupportedDeviceStates(DeviceState[] supportedDeviceStates) {
             mSupportedDeviceStates = supportedDeviceStates;
-            mListener.onSupportedDeviceStatesChanged(supportedDeviceStates);
+            mListener.onSupportedDeviceStatesChanged(supportedDeviceStates,
+                    SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED);
         }
 
         public void setState(int identifier) {
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java
new file mode 100644
index 0000000..8196d6a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateNotificationControllerTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicestate;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+/**
+ * Unit tests for {@link DeviceStateNotificationController}.
+ * <p/>
+ * Run with <code>atest com.android.server.devicestate.DeviceStateNotificationControllerTest</code>.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class DeviceStateNotificationControllerTest {
+
+    private static final int STATE_WITHOUT_NOTIFICATION = 1;
+    private static final int STATE_WITH_ACTIVE_NOTIFICATION = 2;
+    private static final int STATE_WITH_ACTIVE_AND_THERMAL_NOTIFICATION = 3;
+
+    private static final int VALID_APP_UID = 1000;
+    private static final int INVALID_APP_UID = 2000;
+    private static final String VALID_APP_NAME = "Valid app name";
+    private static final String INVALID_APP_NAME = "Invalid app name";
+    private static final String VALID_APP_LABEL = "Valid app label";
+
+    private static final String NAME_1 = "name1";
+    private static final String TITLE_1 = "title1";
+    private static final String CONTENT_1 = "content1:%1$s";
+    private static final String NAME_2 = "name2";
+    private static final String TITLE_2 = "title2";
+    private static final String CONTENT_2 = "content2:%1$s";
+    private static final String THERMAL_TITLE_2 = "thermal_title2";
+    private static final String THERMAL_CONTENT_2 = "thermal_content2";
+
+    private DeviceStateNotificationController mController;
+
+    private final ArgumentCaptor<Notification> mNotificationCaptor = ArgumentCaptor.forClass(
+            Notification.class);
+    private final NotificationManager mNotificationManager = mock(NotificationManager.class);
+
+    @Before
+    public void setup() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        Handler handler = mock(Handler.class);
+        PackageManager packageManager = mock(PackageManager.class);
+        Runnable cancelStateRunnable = mock(Runnable.class);
+        ApplicationInfo applicationInfo = mock(ApplicationInfo.class);
+
+        final SparseArray<DeviceStateNotificationController.NotificationInfo> notificationInfos =
+                new SparseArray<>();
+        notificationInfos.put(STATE_WITH_ACTIVE_NOTIFICATION,
+                new DeviceStateNotificationController.NotificationInfo(
+                        NAME_1, TITLE_1, CONTENT_1,
+                        "", ""));
+        notificationInfos.put(STATE_WITH_ACTIVE_AND_THERMAL_NOTIFICATION,
+                new DeviceStateNotificationController.NotificationInfo(
+                        NAME_2, TITLE_2, CONTENT_2,
+                        THERMAL_TITLE_2, THERMAL_CONTENT_2));
+
+        when(packageManager.getNameForUid(VALID_APP_UID)).thenReturn(VALID_APP_NAME);
+        when(packageManager.getNameForUid(INVALID_APP_UID)).thenReturn(INVALID_APP_NAME);
+        when(packageManager.getApplicationInfo(eq(VALID_APP_NAME), ArgumentMatchers.any()))
+                .thenReturn(applicationInfo);
+        when(packageManager.getApplicationInfo(eq(INVALID_APP_NAME), ArgumentMatchers.any()))
+                .thenThrow(new PackageManager.NameNotFoundException());
+        when(applicationInfo.loadLabel(eq(packageManager))).thenReturn(VALID_APP_LABEL);
+
+        mController = new DeviceStateNotificationController(
+                context, handler, cancelStateRunnable, notificationInfos,
+                packageManager, mNotificationManager);
+    }
+
+    @Test
+    public void test_activeNotification() {
+        mController.showStateActiveNotificationIfNeeded(
+                STATE_WITH_ACTIVE_NOTIFICATION, VALID_APP_UID);
+
+        // Verify that the notification manager is called with correct notification information.
+        verify(mNotificationManager).notify(
+                eq(DeviceStateNotificationController.NOTIFICATION_TAG),
+                eq(DeviceStateNotificationController.NOTIFICATION_ID),
+                mNotificationCaptor.capture());
+        Notification notification = mNotificationCaptor.getValue();
+        assertEquals(TITLE_1, notification.extras.getString(Notification.EXTRA_TITLE));
+        assertEquals(String.format(CONTENT_1, VALID_APP_LABEL),
+                notification.extras.getString(Notification.EXTRA_TEXT));
+        assertEquals(Notification.FLAG_ONGOING_EVENT,
+                notification.flags & Notification.FLAG_ONGOING_EVENT);
+
+        // Verify that the notification action is as expected.
+        Notification.Action[] actions = notification.actions;
+        assertEquals(1, actions.length);
+        Notification.Action action = actions[0];
+        assertEquals(DeviceStateNotificationController.INTENT_ACTION_CANCEL_STATE,
+                action.actionIntent.getIntent().getAction());
+
+        // Verify that the notification is properly canceled.
+        mController.cancelNotification(STATE_WITH_ACTIVE_NOTIFICATION);
+        verify(mNotificationManager).cancel(
+                DeviceStateNotificationController.NOTIFICATION_TAG,
+                DeviceStateNotificationController.NOTIFICATION_ID);
+    }
+
+    @Test
+    public void test_thermalNotification() {
+        // Verify that the active notification is created.
+        mController.showStateActiveNotificationIfNeeded(
+                STATE_WITH_ACTIVE_AND_THERMAL_NOTIFICATION, VALID_APP_UID);
+        verify(mNotificationManager).notify(
+                eq(DeviceStateNotificationController.NOTIFICATION_TAG),
+                eq(DeviceStateNotificationController.NOTIFICATION_ID),
+                mNotificationCaptor.capture());
+        Notification notification = mNotificationCaptor.getValue();
+        assertEquals(TITLE_2, notification.extras.getString(Notification.EXTRA_TITLE));
+        assertEquals(String.format(CONTENT_2, VALID_APP_LABEL),
+                notification.extras.getString(Notification.EXTRA_TEXT));
+        assertEquals(Notification.FLAG_ONGOING_EVENT,
+                notification.flags & Notification.FLAG_ONGOING_EVENT);
+        Mockito.clearInvocations(mNotificationManager);
+
+        // Verify that the thermal critical notification is created.
+        mController.showThermalCriticalNotificationIfNeeded(
+                STATE_WITH_ACTIVE_AND_THERMAL_NOTIFICATION);
+        verify(mNotificationManager).notify(
+                eq(DeviceStateNotificationController.NOTIFICATION_TAG),
+                eq(DeviceStateNotificationController.NOTIFICATION_ID),
+                mNotificationCaptor.capture());
+        notification = mNotificationCaptor.getValue();
+        assertEquals(THERMAL_TITLE_2, notification.extras.getString(Notification.EXTRA_TITLE));
+        assertEquals(THERMAL_CONTENT_2, notification.extras.getString(Notification.EXTRA_TEXT));
+        assertEquals(0, notification.flags & Notification.FLAG_ONGOING_EVENT);
+
+        // Verify that the notification is canceled.
+        mController.cancelNotification(STATE_WITH_ACTIVE_NOTIFICATION);
+        verify(mNotificationManager).cancel(
+                DeviceStateNotificationController.NOTIFICATION_TAG,
+                DeviceStateNotificationController.NOTIFICATION_ID);
+    }
+
+    @Test
+    public void test_deviceStateWithoutNotification() {
+        // Verify that no notification is created.
+        mController.showStateActiveNotificationIfNeeded(
+                STATE_WITHOUT_NOTIFICATION, VALID_APP_UID);
+        verify(mNotificationManager, Mockito.never()).notify(
+                eq(DeviceStateNotificationController.NOTIFICATION_TAG),
+                eq(DeviceStateNotificationController.NOTIFICATION_ID),
+                mNotificationCaptor.capture());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
index 430504c..fe37f42 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
@@ -58,7 +58,7 @@
 
     @Test
     public void addRequest() {
-        OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
         assertNull(mStatusListener.getLastStatus(request));
 
@@ -68,14 +68,14 @@
 
     @Test
     public void addRequest_cancelExistingRequestThroughNewRequest() {
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
         assertNull(mStatusListener.getLastStatus(firstRequest));
 
         mController.addRequest(firstRequest);
         assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
 
-        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
         assertNull(mStatusListener.getLastStatus(secondRequest));
 
@@ -86,7 +86,7 @@
 
     @Test
     public void addRequest_cancelActiveRequest() {
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
 
         mController.addRequest(firstRequest);
@@ -100,7 +100,7 @@
 
     @Test
     public void addBaseStateRequest() {
-        OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest request = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
         assertNull(mStatusListener.getLastStatus(request));
 
@@ -110,14 +110,14 @@
 
     @Test
     public void addBaseStateRequest_cancelExistingBaseStateRequestThroughNewRequest() {
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
         assertNull(mStatusListener.getLastStatus(firstRequest));
 
         mController.addBaseStateRequest(firstRequest);
         assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
 
-        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest secondRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
         assertNull(mStatusListener.getLastStatus(secondRequest));
 
@@ -128,7 +128,7 @@
 
     @Test
     public void addBaseStateRequest_cancelActiveBaseStateRequest() {
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
 
         mController.addBaseStateRequest(firstRequest);
@@ -142,12 +142,13 @@
 
     @Test
     public void handleBaseStateChanged() {
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */,
                 DeviceStateRequest.FLAG_CANCEL_WHEN_BASE_CHANGES /* flags */,
                 OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
 
         OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* uid */,
                 0 /* requestedState */,
                 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
 
@@ -167,10 +168,11 @@
 
     @Test
     public void handleProcessDied() {
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
 
         OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* uid */,
                 1 /* requestedState */,
                 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
 
@@ -189,10 +191,11 @@
     public void handleProcessDied_stickyRequests() {
         mController.setStickyRequestsAllowed(true);
 
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 0 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
 
         OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* uid */,
                 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
 
         mController.addRequest(firstRequest);
@@ -211,10 +214,11 @@
 
     @Test
     public void handleNewSupportedStates() {
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
 
         OverrideRequest baseStateRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+                0 /* uid */,
                 1 /* requestedState */,
                 0 /* flags */, OVERRIDE_REQUEST_TYPE_BASE_STATE);
 
@@ -224,18 +228,20 @@
         mController.addBaseStateRequest(baseStateRequest);
         assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE);
 
-        mController.handleNewSupportedStates(new int[]{0, 1});
+        mController.handleNewSupportedStates(new int[]{0, 1},
+                DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT);
         assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_ACTIVE);
         assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_ACTIVE);
 
-        mController.handleNewSupportedStates(new int[]{0});
+        mController.handleNewSupportedStates(new int[]{0},
+                DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT);
         assertEquals(mStatusListener.getLastStatus(firstRequest).intValue(), STATUS_CANCELED);
         assertEquals(mStatusListener.getLastStatus(baseStateRequest).intValue(), STATUS_CANCELED);
     }
 
     @Test
     public void cancelOverrideRequestsTest() {
-        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */,
+        OverrideRequest firstRequest = new OverrideRequest(new Binder(), 0 /* pid */, 0 /* uid */,
                 1 /* requestedState */, 0 /* flags */, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
 
         mController.addRequest(firstRequest);
@@ -250,7 +256,7 @@
         private Map<OverrideRequest, Integer> mLastStatusMap = new HashMap<>();
 
         @Override
-        public void onStatusChanged(@NonNull OverrideRequest request, int newStatus) {
+        public void onStatusChanged(@NonNull OverrideRequest request, int newStatus, int flags) {
             mLastStatusMap.put(request, newStatus);
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 2d252cb..9a43762 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -157,7 +157,7 @@
        VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot, Context context,
                Handler handler, DisplayAdapter.Listener displayAdapterListener) {
            return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
-                   (String name, boolean secure) -> mMockDisplayToken);
+                   (String name, boolean secure, float refreshRate) -> mMockDisplayToken);
        }
 
        @Override
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index 7fac9b6..7125796 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -19,6 +19,9 @@
 
 import static android.content.Context.SENSOR_SERVICE;
 
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL;
 import static com.android.server.policy.DeviceStateProviderImpl.DEFAULT_DEVICE_STATE;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -124,7 +127,8 @@
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
 
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         assertArrayEquals(new DeviceState[]{DEFAULT_DEVICE_STATE},
                 mDeviceStateArrayCaptor.getValue());
 
@@ -151,7 +155,8 @@
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
 
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         final DeviceState[] expectedStates = new DeviceState[]{
                 new DeviceState(1, "", 0 /* flags */),
                 new DeviceState(2, "", 0 /* flags */) };
@@ -183,7 +188,8 @@
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
 
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         final DeviceState[] expectedStates = new DeviceState[]{
                 new DeviceState(1, "", DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS),
                 new DeviceState(2, "", 0 /* flags */) };
@@ -212,7 +218,8 @@
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
 
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         final DeviceState[] expectedStates = new DeviceState[]{
                 new DeviceState(1, "", 0 /* flags */),
                 new DeviceState(2, "", 0 /* flags */) };
@@ -247,7 +254,8 @@
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
 
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         final DeviceState[] expectedStates = new DeviceState[]{
                 new DeviceState(1, "", 0 /* flags */),
                 new DeviceState(2, "CLOSED", 0 /* flags */) };
@@ -265,7 +273,8 @@
         Mockito.clearInvocations(listener);
 
         provider.notifyLidSwitchChanged(1, true /* lidOpen */);
-        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         verify(listener).onStateChanged(mIntegerCaptor.capture());
         assertEquals(1, mIntegerCaptor.getValue().intValue());
     }
@@ -336,7 +345,8 @@
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
 
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         assertArrayEquals(
                 new DeviceState[]{
                         new DeviceState(1, "CLOSED", 0 /* flags */),
@@ -358,7 +368,8 @@
 
         provider.onSensorChanged(event0);
 
-        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         verify(listener).onStateChanged(mIntegerCaptor.capture());
         assertEquals(3, mIntegerCaptor.getValue().intValue());
 
@@ -370,7 +381,8 @@
 
         provider.onSensorChanged(event1);
 
-        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         verify(listener).onStateChanged(mIntegerCaptor.capture());
         assertEquals(2, mIntegerCaptor.getValue().intValue());
 
@@ -382,7 +394,8 @@
 
         provider.onSensorChanged(event2);
 
-        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         verify(listener).onStateChanged(mIntegerCaptor.capture());
         assertEquals(1, mIntegerCaptor.getValue().intValue());
     }
@@ -397,7 +410,8 @@
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
 
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         assertArrayEquals(
                 new DeviceState[]{
                         new DeviceState(1, "CLOSED", 0 /* flags */),
@@ -410,12 +424,14 @@
         Mockito.clearInvocations(listener);
 
         provider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_MODERATE);
-        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         Mockito.clearInvocations(listener);
 
         // The THERMAL_TEST state should be disabled.
         provider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_CRITICAL);
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL));
         assertArrayEquals(
                 new DeviceState[]{
                         new DeviceState(1, "CLOSED", 0 /* flags */),
@@ -426,7 +442,8 @@
 
         // The THERMAL_TEST state should be re-enabled.
         provider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_LIGHT);
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL));
         assertArrayEquals(
                 new DeviceState[]{
                         new DeviceState(1, "CLOSED", 0 /* flags */),
@@ -468,7 +485,8 @@
 
         provider.onSensorChanged(event2);
 
-        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         verify(listener, never()).onStateChanged(mIntegerCaptor.capture());
     }
 
@@ -513,7 +531,8 @@
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
 
-        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
         assertArrayEquals(
                 new DeviceState[]{
                         new DeviceState(1, "CLOSED", 0 /* flags */),
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 49edde5..12f124e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -525,6 +525,13 @@
 
     @Test
     public void testZenUpgradeNotification() {
+        /**
+         * Commit a485ec65b5ba947d69158ad90905abf3310655cf disabled DND status change
+         * notification on watches. So, assume that the device is not watch.
+         */
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
+
         // shows zen upgrade notification if stored settings says to shows,
         // zen has not been updated, boot is completed
         // and we're setting zen mode on
diff --git a/services/voiceinteraction/Android.bp b/services/voiceinteraction/Android.bp
index af0eca9..7332d2d 100644
--- a/services/voiceinteraction/Android.bp
+++ b/services/voiceinteraction/Android.bp
@@ -20,6 +20,7 @@
     srcs: [":services.voiceinteraction-sources"],
     libs: [
         "services.core",
+        "app-compat-annotations",
         "android.hardware.power-V1-java",
         "android.hardware.power-V1.0-java",
     ],
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 03796de..cc22847 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -51,7 +51,6 @@
 import android.os.ServiceManager;
 import android.os.SharedMemory;
 import android.provider.DeviceConfig;
-import android.service.voice.HotwordDetectedResult;
 import android.service.voice.HotwordDetectionService;
 import android.service.voice.HotwordDetector;
 import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
@@ -89,19 +88,18 @@
     static final boolean DEBUG = false;
 
     /**
-     * For apps targeting Android API {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above,
-     * implementors of the {@link HotwordDetectionService} must not augment the phrase IDs which are
-     * supplied via {@link HotwordDetectionService
-     * #onDetect(AlwaysOnHotwordDetector.EventPayload, long, HotwordDetectionService.Callback)}.
+     * For apps targeting Android API Build.VERSION_CODES#UPSIDE_DOWN_CAKE and above,
+     * implementors of the HotwordDetectionService must not augment the phrase IDs which are
+     * supplied via HotwordDetectionService
+     * #onDetect(AlwaysOnHotwordDetector.EventPayload, long, HotwordDetectionService.Callback).
      *
-     * <p>The {@link HotwordDetectedResult#getHotwordPhraseId()} must match one of the phrase IDs
-     * from the {@link android.service.voice.AlwaysOnHotwordDetector
-     * .EventPayload#getKeyphraseRecognitionExtras()} list.
+     * <p>The HotwordDetectedResult#getHotwordPhraseId() must match one of the phrase IDs
+     * from the AlwaysOnHotwordDetector.EventPayload#getKeyphraseRecognitionExtras() list.
      * </p>
      *
-     * <p>This behavior change is made to ensure the {@link HotwordDetectionService} honors what
-     * it receives from the {@link android.hardware.soundtrigger.SoundTriggerModule}, and it
-     * cannot signal to the client application a phrase which was not origially detected.
+     * <p>This behavior change is made to ensure the HotwordDetectionService honors what
+     * it receives from the android.hardware.soundtrigger.SoundTriggerModule, and it
+     * cannot signal to the client application a phrase which was not originally detected.
      * </p>
      */
     @ChangeId
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index f0f230f..acfc194 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -3866,13 +3866,12 @@
      * NR_SA - NR SA is unmetered for sub-6 frequencies
      * NR_SA_MMWAVE - NR SA is unmetered for mmwave frequencies
      *
-     * Note that this config only applies if an unmetered SubscriptionPlan is set via
-     * {@link SubscriptionManager#setSubscriptionPlans(int, List)} or an unmetered override is set
+     * Note that this config only applies if an unmetered SubscriptionPlan is set via {@link
+     * SubscriptionManager#setSubscriptionPlans(int, List, long)} or an unmetered override is set
      * via {@link SubscriptionManager#setSubscriptionOverrideUnmetered(int, boolean, int[], long)}
      * or {@link SubscriptionManager#setSubscriptionOverrideUnmetered(int, boolean, long)}.
      * If neither SubscriptionPlans nor an override are set, then no network types can be unmetered
      * regardless of the value of this config.
-     * TODO: remove other unmetered keys and replace with this
      * @hide
      */
     public static final String KEY_UNMETERED_NETWORK_TYPES_STRING_ARRAY =
@@ -3887,73 +3886,18 @@
      * NR_SA - NR SA is unmetered when roaming for sub-6 frequencies
      * NR_SA_MMWAVE - NR SA is unmetered when roaming for mmwave frequencies
      *
-     * Note that this config only applies if an unmetered SubscriptionPlan is set via
-     * {@link SubscriptionManager#setSubscriptionPlans(int, List)} or an unmetered override is set
+     * Note that this config only applies if an unmetered SubscriptionPlan is set via {@link
+     * SubscriptionManager#setSubscriptionPlans(int, List, long)} or an unmetered override is set
      * via {@link SubscriptionManager#setSubscriptionOverrideUnmetered(int, boolean, int[], long)}
      * or {@link SubscriptionManager#setSubscriptionOverrideUnmetered(int, boolean, long)}.
      * If neither SubscriptionPlans nor an override are set, then no network types can be unmetered
      * when roaming regardless of the value of this config.
-     * TODO: remove KEY_UNMETERED_NR_NSA_WHEN_ROAMING_BOOL and replace with this
      * @hide
      */
     public static final String KEY_ROAMING_UNMETERED_NETWORK_TYPES_STRING_ARRAY =
             "roaming_unmetered_network_types_string_array";
 
     /**
-     * Whether NR (non-standalone) should be unmetered for all frequencies.
-     * If either {@link #KEY_UNMETERED_NR_NSA_MMWAVE_BOOL} or
-     * {@link #KEY_UNMETERED_NR_NSA_SUB6_BOOL} are true, then this value will be ignored.
-     * @hide
-     */
-    public static final String KEY_UNMETERED_NR_NSA_BOOL = "unmetered_nr_nsa_bool";
-
-    /**
-     * Whether NR (non-standalone) frequencies above 6GHz (millimeter wave) should be unmetered.
-     * If this is true, then the value for {@link #KEY_UNMETERED_NR_NSA_BOOL} will be ignored.
-     * @hide
-     */
-    public static final String KEY_UNMETERED_NR_NSA_MMWAVE_BOOL = "unmetered_nr_nsa_mmwave_bool";
-
-    /**
-     * Whether NR (non-standalone) frequencies below 6GHz (sub6) should be unmetered.
-     * If this is true, then the value for {@link #KEY_UNMETERED_NR_NSA_BOOL} will be ignored.
-     * @hide
-     */
-    public static final String KEY_UNMETERED_NR_NSA_SUB6_BOOL = "unmetered_nr_nsa_sub6_bool";
-
-    /**
-     * Whether NR (non-standalone) should be unmetered when the device is roaming.
-     * If false, then the values for {@link #KEY_UNMETERED_NR_NSA_BOOL},
-     * {@link #KEY_UNMETERED_NR_NSA_MMWAVE_BOOL}, {@link #KEY_UNMETERED_NR_NSA_SUB6_BOOL},
-     * and unmetered {@link SubscriptionPlan} will be ignored.
-     * @hide
-     */
-    public static final String KEY_UNMETERED_NR_NSA_WHEN_ROAMING_BOOL =
-            "unmetered_nr_nsa_when_roaming_bool";
-
-    /**
-     * Whether NR (standalone) should be unmetered for all frequencies.
-     * If either {@link #KEY_UNMETERED_NR_SA_MMWAVE_BOOL} or
-     * {@link #KEY_UNMETERED_NR_SA_SUB6_BOOL} are true, then this value will be ignored.
-     * @hide
-     */
-    public static final String KEY_UNMETERED_NR_SA_BOOL = "unmetered_nr_sa_bool";
-
-    /**
-     * Whether NR (standalone) frequencies above 6GHz (millimeter wave) should be unmetered.
-     * If this is true, then the value for {@link #KEY_UNMETERED_NR_SA_BOOL} will be ignored.
-     * @hide
-     */
-    public static final String KEY_UNMETERED_NR_SA_MMWAVE_BOOL = "unmetered_nr_sa_mmwave_bool";
-
-    /**
-     * Whether NR (standalone) frequencies below 6GHz (sub6) should be unmetered.
-     * If this is true, then the value for {@link #KEY_UNMETERED_NR_SA_BOOL} will be ignored.
-     * @hide
-     */
-    public static final String KEY_UNMETERED_NR_SA_SUB6_BOOL = "unmetered_nr_sa_sub6_bool";
-
-    /**
      * Support ASCII 7-BIT encoding for long SMS. This carrier config is used to enable
      * this feature.
      * @hide
@@ -10043,13 +9987,6 @@
         sDefaults.putStringArray(KEY_UNMETERED_NETWORK_TYPES_STRING_ARRAY, new String[] {
                 "NR_NSA", "NR_NSA_MMWAVE", "NR_SA", "NR_SA_MMWAVE"});
         sDefaults.putStringArray(KEY_ROAMING_UNMETERED_NETWORK_TYPES_STRING_ARRAY, new String[0]);
-        sDefaults.putBoolean(KEY_UNMETERED_NR_NSA_BOOL, false);
-        sDefaults.putBoolean(KEY_UNMETERED_NR_NSA_MMWAVE_BOOL, false);
-        sDefaults.putBoolean(KEY_UNMETERED_NR_NSA_SUB6_BOOL, false);
-        sDefaults.putBoolean(KEY_UNMETERED_NR_NSA_WHEN_ROAMING_BOOL, false);
-        sDefaults.putBoolean(KEY_UNMETERED_NR_SA_BOOL, false);
-        sDefaults.putBoolean(KEY_UNMETERED_NR_SA_MMWAVE_BOOL, false);
-        sDefaults.putBoolean(KEY_UNMETERED_NR_SA_SUB6_BOOL, false);
         sDefaults.putBoolean(KEY_ASCII_7_BIT_SUPPORT_FOR_LONG_MESSAGE_BOOL, false);
         sDefaults.putBoolean(KEY_SHOW_WIFI_CALLING_ICON_IN_STATUS_BAR_BOOL, false);
         sDefaults.putBoolean(KEY_CARRIER_SUPPORTS_OPP_DATA_AUTO_PROVISIONING_BOOL, false);
diff --git a/telephony/java/android/telephony/TelephonyFrameworkInitializer.java b/telephony/java/android/telephony/TelephonyFrameworkInitializer.java
index 8308821..c2f5b8f 100644
--- a/telephony/java/android/telephony/TelephonyFrameworkInitializer.java
+++ b/telephony/java/android/telephony/TelephonyFrameworkInitializer.java
@@ -23,6 +23,7 @@
 import android.telephony.euicc.EuiccCardManager;
 import android.telephony.euicc.EuiccManager;
 import android.telephony.ims.ImsManager;
+import android.telephony.satellite.SatelliteManager;
 
 import com.android.internal.util.Preconditions;
 
@@ -98,6 +99,11 @@
                 context -> SmsManager.getSmsManagerForContextAndSubscriptionId(context,
                         SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
         );
+        SystemServiceRegistry.registerContextAwareService(
+                Context.SATELLITE_SERVICE,
+                SatelliteManager.class,
+                context -> new SatelliteManager(context)
+        );
     }
 
     /** @hide */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 0921834..c65faea 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -9432,7 +9432,6 @@
             ALLOWED_NETWORK_TYPES_REASON_POWER,
             ALLOWED_NETWORK_TYPES_REASON_CARRIER,
             ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G,
-            ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AllowedNetworkTypesReason {
@@ -9471,15 +9470,6 @@
     public static final int ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G = 3;
 
     /**
-     * To indicate allowed network type change is requested by an update to the
-     * {@link android.os.UserManager.DISALLOW_CELLULAR_2G} user restriction.
-     *
-     * @hide
-     */
-    @SystemApi
-    public static final int ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS = 4;
-
-    /**
      * Set the allowed network types of the device and provide the reason triggering the allowed
      * network change.
      * <p>Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or
@@ -9503,7 +9493,6 @@
      * {@link android.Manifest.permission#MODIFY_PHONE_STATE} permissions.
      * <ol>
      *     <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G}</li>
-     *     <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS}</li>
      * </ol>
      */
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -9578,7 +9567,6 @@
             case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_POWER:
             case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_CARRIER:
             case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G:
-            case ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS:
                 return true;
         }
         return false;
diff --git a/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl b/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
new file mode 100644
index 0000000..5c3fa32
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/ISatellitePositionUpdateCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+import android.telephony.satellite.PointingInfo;
+
+/**
+ * Callback for position updates from the satellite service.
+ * @hide
+ */
+oneway interface ISatellitePositionUpdateCallback {
+    void onSatellitePositionUpdate(in PointingInfo pointingInfo);
+    void onMessageTransferStateUpdate(in int state);
+}
diff --git a/telephony/java/android/telephony/satellite/PointingInfo.aidl b/telephony/java/android/telephony/satellite/PointingInfo.aidl
new file mode 100644
index 0000000..7ff95cd
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/PointingInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+parcelable PointingInfo;
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
new file mode 100644
index 0000000..d015427
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyFrameworkInitializer;
+import android.util.ArrayMap;
+
+import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.ITelephony;
+import com.android.telephony.Rlog;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+/**
+ * Manages satellite operations such as provisioning, pointing, messaging, location sharing, etc.
+ * To get the object, call {@link Context#getSystemService(Context.SATELLITE_SERVICE)}.
+ * To create an instance of {@link SatelliteManager} associated with a specific subscription ID,
+ * call {@link #createForSubscriptionId(int)}.
+ *
+ * @hide
+ */
+@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SATELLITE)
+public class SatelliteManager {
+    private static final String TAG = "SatelliteManager";
+
+    /**
+     * Map of all SatellitePositionUpdateCallback and their associated callback ids.
+     */
+    private final Map<SatellitePositionUpdateCallback, Integer> mSatellitePositionUpdateCallbacks =
+            new ArrayMap<>();
+
+    /**
+     * AtomicInteger for the id of the next SatellitePositionUpdateCallback.
+     */
+    private final AtomicInteger mSatellitePositionUpdateCallbackId = new AtomicInteger(0);
+
+    /**
+     * The subscription ID for this SatelliteManager.
+     */
+    private final int mSubId;
+
+    /**
+     * Context this SatelliteManager is for.
+     */
+    @Nullable private final Context mContext;
+
+    /**
+     * Create an instance of the SatelliteManager.
+     *
+     * @param context The context the SatelliteManager belongs to.
+     */
+    public SatelliteManager(@Nullable Context context) {
+        // TODO: replace DEFAULT_SUBSCRIPTION_ID with DEFAULT_SATELLITE_SUBSCRIPTION_ID
+        this(context, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
+    }
+
+    /**
+     * Create a new SatelliteManager associated with the given subscription ID.
+     *
+     * @param subId The subscription ID to create the SatelliteManager with.
+     * @return A SatelliteManager that uses the given subscription ID for all calls.
+     */
+    @NonNull public SatelliteManager createForSubscriptionId(int subId) {
+        return new SatelliteManager(mContext, subId);
+    }
+
+    /**
+     * Create an instance of the SatelliteManager associated with a particular subscription.
+     *
+     * @param context The context the SatelliteManager belongs to.
+     * @param subId The subscription ID associated with the SatelliteManager.
+     */
+    private SatelliteManager(@Nullable Context context, int subId) {
+        mContext = context;
+        mSubId = subId;
+    }
+
+    /**
+     * Successful response.
+     */
+    public static final int SATELLITE_SERVICE_SUCCESS = 0;
+    /**
+     * Satellite server is not reachable.
+     */
+    public static final int SATELLITE_SERVICE_SERVER_NOT_REACHABLE = 1;
+    /**
+     * Error received from the satellite server.
+     */
+    public static final int SATELLITE_SERVICE_SERVER_ERROR = 2;
+    /**
+     * Unexpected telephony internal error.
+     */
+    public static final int SATELLITE_SERVICE_TELEPHONY_INTERNAL_ERROR = 3;
+    /**
+     * Modem error received from the satellite service.
+     */
+    public static final int SATELLITE_SERVICE_MODEM_ERROR = 4;
+    /**
+     * System error received from the satellite service.
+     */
+    public static final int SATELLITE_SERVICE_SYSTEM_ERROR = 5;
+    /**
+     * Invalid arguments passed.
+     */
+    public static final int SATELLITE_SERVICE_INVALID_ARGUMENTS = 6;
+    /**
+     * Invalid modem state.
+     */
+    public static final int SATELLITE_SERVICE_INVALID_MODEM_STATE = 7;
+    /**
+     * Invalid SIM state.
+     */
+    public static final int SATELLITE_SERVICE_INVALID_SIM_STATE = 8;
+    /**
+     * Invalid state.
+     */
+    public static final int SATELLITE_SERVICE_INVALID_STATE = 9;
+    /**
+     * Satellite service is unavailable.
+     */
+    public static final int SATELLITE_SERVICE_NOT_AVAILABLE = 10;
+    /**
+     * Satellite service is not supported by the device or OS.
+     */
+    public static final int SATELLITE_SERVICE_NOT_SUPPORTED = 11;
+    /**
+     * Satellite service is rate limited.
+     */
+    public static final int SATELLITE_SERVICE_RATE_LIMITED = 12;
+    /**
+     * Satellite service has no memory available.
+     */
+    public static final int SATELLITE_SERVICE_NO_MEMORY = 13;
+    /**
+     * Satellite service has no resources available.
+     */
+    public static final int SATELLITE_SERVICE_NO_RESOURCES = 14;
+    /**
+     * Failed to send a request to the satellite service.
+     */
+    public static final int SATELLITE_SERVICE_REQUEST_FAILED = 15;
+    /**
+     * Failed to send a request to the satellite service for the given subscription ID.
+     */
+    public static final int SATELLITE_SERVICE_INVALID_SUBSCRIPTION_ID = 16;
+    /**
+     * Error received from satellite service.
+     */
+    public static final int SATELLITE_SERVICE_ERROR = 17;
+
+    /**
+     * Satellite service is disabled on the requested subscription.
+     */
+    public static final int SATELLITE_SERVICE_DISABLED = 18;
+
+    /** @hide */
+    @IntDef(prefix = {"SATELLITE_SERVICE_"}, value = {
+            SATELLITE_SERVICE_SUCCESS,
+            SATELLITE_SERVICE_SERVER_NOT_REACHABLE,
+            SATELLITE_SERVICE_SERVER_ERROR,
+            SATELLITE_SERVICE_TELEPHONY_INTERNAL_ERROR,
+            SATELLITE_SERVICE_MODEM_ERROR,
+            SATELLITE_SERVICE_SYSTEM_ERROR,
+            SATELLITE_SERVICE_INVALID_ARGUMENTS,
+            SATELLITE_SERVICE_INVALID_MODEM_STATE,
+            SATELLITE_SERVICE_INVALID_SIM_STATE,
+            SATELLITE_SERVICE_INVALID_STATE,
+            SATELLITE_SERVICE_NOT_AVAILABLE,
+            SATELLITE_SERVICE_NOT_SUPPORTED,
+            SATELLITE_SERVICE_RATE_LIMITED,
+            SATELLITE_SERVICE_NO_MEMORY,
+            SATELLITE_SERVICE_NO_RESOURCES,
+            SATELLITE_SERVICE_REQUEST_FAILED,
+            SATELLITE_SERVICE_INVALID_SUBSCRIPTION_ID,
+            SATELLITE_SERVICE_ERROR,
+            SATELLITE_SERVICE_DISABLED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SatelliteServiceResult {}
+
+    /**
+     * Message transfer is waiting to acquire.
+     */
+    public static final int SATELLITE_MESSAGE_TRANSFER_STATE_WAITING_TO_ACQUIRE = 0;
+    /**
+     * Message is being sent.
+     */
+    public static final int SATELLITE_MESSAGE_TRANSFER_STATE_SENDING = 1;
+    /**
+     * Message is being received.
+     */
+    public static final int SATELLITE_MESSAGE_TRANSFER_STATE_RECEIVING = 2;
+    /**
+     * Message transfer is being retried.
+     */
+    public static final int SATELLITE_MESSAGE_TRANSFER_STATE_RETRYING = 3;
+    /**
+     * Message transfer is complete.
+     */
+    public static final int SATELLITE_MESSAGE_TRANSFER_STATE_COMPLETE = 4;
+
+    /** @hide */
+    @IntDef(prefix = {"SATELLITE_MESSAGE_TRANSFER_STATE_"}, value = {
+            SATELLITE_MESSAGE_TRANSFER_STATE_WAITING_TO_ACQUIRE,
+            SATELLITE_MESSAGE_TRANSFER_STATE_SENDING,
+            SATELLITE_MESSAGE_TRANSFER_STATE_RECEIVING,
+            SATELLITE_MESSAGE_TRANSFER_STATE_RETRYING,
+            SATELLITE_MESSAGE_TRANSFER_STATE_COMPLETE
+    })
+    public @interface SatelliteMessageTransferState {}
+
+    /**
+     * Callback for position updates from the satellite service.
+     */
+    public interface SatellitePositionUpdateCallback {
+        /**
+         * Called when the satellite position changes.
+         *
+         * @param pointingInfo The pointing info containing the satellite location.
+         */
+        void onSatellitePositionUpdate(@NonNull PointingInfo pointingInfo);
+
+        /**
+         * Called when satellite message transfer state changes.
+         *
+         * @param state The new message transfer state.
+         */
+        void onMessageTransferStateUpdate(@SatelliteMessageTransferState int state);
+    }
+
+    /**
+     * Start receiving satellite position updates.
+     * This can be called by the pointing UI when the user starts pointing to the satellite.
+     * Modem should continue to report the pointing input as the device or satellite moves.
+     * Satellite position updates are started only on {@link #SATELLITE_SERVICE_SUCCESS}.
+     * All other results indicate that this operation failed.
+     *
+     * @param executor The executor to run callbacks on.
+     * @param callback The callback to notify of changes in satellite position.
+     * @return The result of the operation.
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @SatelliteServiceResult public int startSatellitePositionUpdates(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull SatellitePositionUpdateCallback callback) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                int id;
+                if (mSatellitePositionUpdateCallbacks.containsKey(callback)) {
+                    id = mSatellitePositionUpdateCallbacks.get(callback);
+                } else {
+                    id = mSatellitePositionUpdateCallbackId.getAndIncrement();
+                }
+                int result = telephony.startSatellitePositionUpdates(mSubId, id,
+                        new ISatellitePositionUpdateCallback.Stub() {
+                            @Override
+                            public void onSatellitePositionUpdate(
+                                    @NonNull PointingInfo pointingInfo) {
+                                logd("onSatellitePositionUpdate: pointingInfo=" + pointingInfo);
+                                executor.execute(() ->
+                                        callback.onSatellitePositionUpdate(pointingInfo));
+                            }
+
+                            @Override
+                            public void onMessageTransferStateUpdate(
+                                    @SatelliteMessageTransferState int state) {
+                                logd("onMessageTransferStateUpdate: state=" + state);
+                                executor.execute(() ->
+                                        callback.onMessageTransferStateUpdate(state));
+                            }
+                        });
+                if (result == SATELLITE_SERVICE_SUCCESS) {
+                    mSatellitePositionUpdateCallbacks.put(callback, id);
+                }
+                return result;
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("startSatellitePositionUpdates RemoteException: " + ex);
+            ex.rethrowFromSystemServer();
+        }
+        return SATELLITE_SERVICE_REQUEST_FAILED;
+    }
+
+    /**
+     * Stop receiving satellite position updates.
+     * This can be called by the pointing UI when the user stops pointing to the satellite.
+     * Satellite position updates are stopped only on {@link #SATELLITE_SERVICE_SUCCESS}.
+     * All other results indicate that this operation failed.
+     *
+     * @param callback The callback that was passed in {@link
+     *                 #startSatellitePositionUpdates(Executor, SatellitePositionUpdateCallback)}.
+     * @return The result of the operation.
+     * @throws IllegalArgumentException if the callback is invalid.
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @SatelliteServiceResult public int stopSatellitePositionUpdates(
+            @NonNull SatellitePositionUpdateCallback callback) {
+        if (!mSatellitePositionUpdateCallbacks.containsKey(callback)) {
+            throw new IllegalArgumentException(
+                    "startSatellitePositionUpdates was never called with the callback provided.");
+        }
+
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                int result = telephony.stopSatellitePositionUpdates(mSubId,
+                        mSatellitePositionUpdateCallbacks.get(callback));
+                if (result == SATELLITE_SERVICE_SUCCESS) {
+                    mSatellitePositionUpdateCallbacks.remove(callback);
+                    // TODO: Notify SmsHandler that pointing UI stopped
+                }
+                return result;
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("stopSatellitePositionUpdates RemoteException: " + ex);
+            ex.rethrowFromSystemServer();
+        }
+        return SATELLITE_SERVICE_REQUEST_FAILED;
+    }
+
+    /**
+     * Get maximum number of characters per text message on satellite.
+     * @param executor - The executor on which the result listener will be called.
+     * @param resultListener - Listener that will be called when the operation is successful.
+     *                       If this method returns {@link #SATELLITE_SERVICE_SUCCESS}, listener
+     *                       will be called with maximum characters limit.
+     *
+     * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @return The result of the operation
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @SatelliteServiceResult
+    public int getMaxCharactersPerSatelliteTextMessage(@NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<Integer> resultListener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(resultListener);
+
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                IIntegerConsumer internalCallback = new IIntegerConsumer.Stub() {
+                    @Override
+                    public void accept(int result) {
+                        executor.execute(() -> Binder.withCleanCallingIdentity(
+                                () -> resultListener.accept(result)));
+                    }
+                };
+
+                return telephony.getMaxCharactersPerSatelliteTextMessage(mSubId, internalCallback);
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("getMaxCharactersPerSatelliteTextMessage() RemoteException:" + ex);
+            ex.rethrowFromSystemServer();
+        }
+        return SATELLITE_SERVICE_REQUEST_FAILED;
+    }
+
+    private static ITelephony getITelephony() {
+        ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
+                .getTelephonyServiceManager()
+                .getTelephonyServiceRegisterer()
+                .get());
+        if (binder == null) {
+            throw new RuntimeException("Could not find Telephony Service.");
+        }
+        return binder;
+    }
+
+    private static void logd(@NonNull String log) {
+        Rlog.d(TAG, log);
+    }
+
+    private static void loge(@NonNull String log) {
+        Rlog.e(TAG, log);
+    }
+}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 48b1657..5486365 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -66,6 +66,7 @@
 import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.IImsRegistrationCallback;
 import android.telephony.ims.aidl.IRcsConfigCallback;
+import android.telephony.satellite.ISatellitePositionUpdateCallback;
 import com.android.ims.internal.IImsServiceFeatureCallback;
 import com.android.internal.telephony.CellNetworkScanResult;
 import com.android.internal.telephony.IBooleanConsumer;
@@ -2636,14 +2637,14 @@
      */
     boolean isRemovableEsimDefaultEuicc(String callingPackage);
 
-     /**
-      * Get the component name of the default app to direct respond-via-message intent for the
-      * user associated with this subscription, update the cache if there is no respond-via-message
-      * application currently configured for this user.
-      * @return component name of the app and class to direct Respond Via Message intent to, or
-      * {@code null} if the functionality is not supported.
-      * @hide
-      */
+    /**
+     * Get the component name of the default app to direct respond-via-message intent for the
+     * user associated with this subscription, update the cache if there is no respond-via-message
+     * application currently configured for this user.
+     * @return component name of the app and class to direct Respond Via Message intent to, or
+     * {@code null} if the functionality is not supported.
+     * @hide
+     */
     ComponentName getDefaultRespondViaMessageApplication(int subId, boolean updateIfNeeded);
 
     /**
@@ -2667,12 +2668,12 @@
     void setNullCipherAndIntegrityEnabled(boolean enabled);
 
     /**
-    * Get whether the radio is able to connect with null ciphering or integrity
-    * algorithms. Note that this retrieves the phone-global preference and not
-    * the state of the radio.
-    *
-    * @hide
-    */
+     * Get whether the radio is able to connect with null ciphering or integrity
+     * algorithms. Note that this retrieves the phone-global preference and not
+     * the state of the radio.
+     *
+     * @hide
+     */
     boolean isNullCipherAndIntegrityPreferenceEnabled();
 
     /**
@@ -2696,5 +2697,21 @@
     /**
      * Get the carrier restriction status of the device.
      */
-     void getCarrierRestrictionStatus(IIntegerConsumer internalCallback, String packageName);
-}
+    void getCarrierRestrictionStatus(IIntegerConsumer internalCallback, String packageName);
+
+    /**
+     * Start receiving satellite pointing updates.
+     */
+    int startSatellitePositionUpdates(int subId, int callbackId,
+            in ISatellitePositionUpdateCallback callback);
+
+    /**
+     * Stop receiving satellite pointing updates.
+     */
+    int stopSatellitePositionUpdates(int subId, int callbackId);
+
+    /**
+     * Get maximum number of characters per text message on satellite.
+     */
+    int getMaxCharactersPerSatelliteTextMessage(int subId, IIntegerConsumer internalCallback);
+}
\ No newline at end of file
diff --git a/tests/BinaryTransparencyHostTest/Android.bp b/tests/BinaryTransparencyHostTest/Android.bp
index 142e3dd..dc6bdff 100644
--- a/tests/BinaryTransparencyHostTest/Android.bp
+++ b/tests/BinaryTransparencyHostTest/Android.bp
@@ -35,6 +35,7 @@
     data: [
         ":BinaryTransparencyTestApp",
         ":EasterEgg",
+        ":com.android.apex.cts.shim.v2_rebootless_prebuilt",
     ],
     test_suites: [
         "general-tests",
diff --git a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
index 84bed92..8db3d00 100644
--- a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
+++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java
@@ -17,6 +17,7 @@
 package android.transparency.test;
 
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
@@ -29,14 +30,22 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.TimeUnit;
+
 // TODO: Add @Presubmit
 @RunWith(DeviceJUnit4ClassRunner.class)
 public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test {
     private static final String PACKAGE_NAME = "android.transparency.test.app";
 
+    private static final String JOB_ID = "1740526926";
+
+    /** Waiting time for the job to be scheduled */
+    private static final int JOB_CREATION_MAX_SECONDS = 5;
+
     @After
     public void tearDown() throws Exception {
         uninstallPackage("com.android.egg");
+        uninstallRebootlessApex();
     }
 
     @Test
@@ -64,6 +73,28 @@
     }
 
     @Test
+    public void testRebootlessApexUpdateTriggersJobScheduling() throws Exception {
+        cancelPendingJob();
+        installRebootlessApex();
+
+        // Verify
+        expectJobToBeScheduled();
+        // Just cancel since we can't verifying very meaningfully.
+        cancelPendingJob();
+    }
+
+    @Test
+    public void testPreloadUpdateTriggersJobScheduling() throws Exception {
+        cancelPendingJob();
+        installPackage("EasterEgg.apk");
+
+        // Verify
+        expectJobToBeScheduled();
+        // Just cancel since we can't verifying very meaningfully.
+        cancelPendingJob();
+    }
+
+    @Test
     public void testMeasureMbas() throws Exception {
         // TODO(265244016): figure out a way to install an MBA
     }
@@ -74,4 +105,47 @@
         options.setTestMethodName(method);
         runDeviceTests(options);
     }
+
+    private void cancelPendingJob() throws DeviceNotAvailableException {
+        CommandResult result = getDevice().executeShellV2Command(
+                "cmd jobscheduler cancel android " + JOB_ID);
+        assertTrue(result.getStatus() == CommandStatus.SUCCESS);
+    }
+
+    private void expectJobToBeScheduled() throws Exception {
+        for (int i = 0; i < JOB_CREATION_MAX_SECONDS; i++) {
+            CommandResult result = getDevice().executeShellV2Command(
+                    "cmd jobscheduler get-job-state android " + JOB_ID);
+            String state = result.getStdout().toString();
+            if (state.startsWith("unknown")) {
+                // The job hasn't been scheduled yet. So try again.
+                TimeUnit.SECONDS.sleep(1);
+            } else if (result.getExitCode() != 0) {
+                fail("Failing due to unexpected job state: " + result);
+            } else {
+                // The job exists, which is all we care about here
+                return;
+            }
+        }
+        fail("Timed out waiting for the job to be scheduled");
+    }
+
+    private void installRebootlessApex() throws Exception {
+        installPackage("com.android.apex.cts.shim.v2_rebootless.apex", "--force-non-staged");
+    }
+
+    private void uninstallRebootlessApex() throws DeviceNotAvailableException {
+        // Reboot only if the APEX is not the pre-install one.
+        CommandResult result = getDevice().executeShellV2Command(
+                "pm list packages -f --apex-only |grep com.android.apex.cts.shim");
+        assertTrue(result.getStatus() == CommandStatus.SUCCESS);
+        if (result.getStdout().contains("/data/apex/active/")) {
+            uninstallPackage("com.android.apex.cts.shim");
+            getDevice().reboot();
+
+            // Reboot enforces SELinux. Make it permissive again.
+            CommandResult runResult = getDevice().executeShellV2Command("setenforce 0");
+            assertTrue(runResult.getStatus() == CommandStatus.SUCCESS);
+        }
+    }
 }