Merge changes Ifc4e989b,I51bda79d into main

* changes:
  Tutorial correctly handling META key event
  Adding logger for touchpad and keyboard tutorials
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index df01aa8..f483691 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3450,7 +3450,7 @@
   }
 
   public static interface VirtualDeviceManager.ActivityListener {
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public default void onActivityLaunchBlocked(int, @NonNull android.content.ComponentName, int, @Nullable android.content.IntentSender);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public default void onActivityLaunchBlocked(int, @NonNull android.content.ComponentName, @NonNull android.os.UserHandle, @Nullable android.content.IntentSender);
     method public void onDisplayEmpty(int);
     method @Deprecated public void onTopActivityChanged(int, @NonNull android.content.ComponentName);
     method public default void onTopActivityChanged(int, @NonNull android.content.ComponentName, int);
@@ -3530,7 +3530,7 @@
     field @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
     field @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
     field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
-    field @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final int POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR = 6; // 0x6
+    field @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6; // 0x6
     field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5
     field @FlaggedApi("android.companion.virtual.flags.cross_device_clipboard") public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4
     field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
@@ -4598,6 +4598,8 @@
     method @NonNull public String getPackageName();
     method @NonNull public android.content.pm.VersionedPackage getVersionRolledBackFrom();
     method @NonNull public android.content.pm.VersionedPackage getVersionRolledBackTo();
+    method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") public boolean isApex();
+    method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") public boolean isApkInApex();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.content.rollback.PackageRollbackInfo> CREATOR;
   }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index db979a5..e99ba84 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1578,6 +1578,22 @@
     public static final String EXTRA_DECLINE_COLOR = "android.declineColor";
 
     /**
+     * {@link #extras} key: {@link Icon} of an image used as an overlay Icon on
+     * {@link Notification#mLargeIcon} for {@link EnRouteStyle} notifications.
+     * This extra is an {@code Icon}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_ENROUTE_OVERLAY_ICON = "android.enrouteOverlayIcon";
+
+    /**
+     * {@link #extras} key: text used as a sub-text for the largeIcon of
+     * {@link EnRouteStyle} notification. This extra is a {@code CharSequence}.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static final String EXTRA_ENROUTE_LARGE_ICON_SUBTEXT = "android.enrouteLargeIconSubText";
+    /**
      * {@link #extras} key: whether the notification should be colorized as
      * supplied to {@link Builder#setColorized(boolean)}.
      */
@@ -3039,6 +3055,10 @@
             visitIconUri(visitor, extras.getParcelable(EXTRA_VERIFICATION_ICON, Icon.class));
         }
 
+        if (Flags.apiRichOngoing()) {
+            visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class));
+        }
+
         if (mBubbleMetadata != null) {
             visitIconUri(visitor, mBubbleMetadata.getIcon());
         }
@@ -10979,6 +10999,144 @@
     }
 
     /**
+     * TODO(b/360827871): Make EnRouteStyle public.
+     * A style used to represent the progress of a real-world journey with a known destination.
+     * For example:
+     * <ul>
+     *     <li>Delivery tracking</li>
+     *     <li>Ride progress</li>
+     *     <li>Flight tracking</li>
+     * </ul>
+     *
+     * The exact fields from {@link Notification} that are shown with this style may vary by
+     * the surface where this update appears, but the following fields are recommended:
+     * <ul>
+     *     <li>{@link Notification.Builder#setContentTitle}</li>
+     *     <li>{@link Notification.Builder#setContentText}</li>
+     *     <li>{@link Notification.Builder#setSubText}</li>
+     *     <li>{@link Notification.Builder#setLargeIcon}</li>
+     *     <li>{@link Notification.Builder#setProgress}</li>
+     *     <li>{@link Notification.Builder#setWhen} - This should be the future time of the next,
+     *     final, or most important stop on this journey.</li>
+     * </ul>
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public static class EnRouteStyle extends Notification.Style {
+
+        @Nullable
+        private Icon mOverlayIcon = null;
+
+        @Nullable
+        private CharSequence mLargeIconSubText = null;
+
+        public EnRouteStyle() {
+        }
+
+        /**
+         * Returns the overlay icon to be displayed on {@link Notification#mLargeIcon}.
+         * @see EnRouteStyle#setOverlayIcon
+         */
+        @Nullable
+        public Icon getOverlayIcon() {
+            return mOverlayIcon;
+        }
+
+        /**
+         * Optional icon to be displayed on {@link Notification#mLargeIcon}.
+         *
+         * This image will be cropped to a circle and will obscure
+         * a semicircle of the right side of the large icon.
+         */
+        @NonNull
+        public EnRouteStyle setOverlayIcon(@Nullable Icon overlayIcon) {
+            mOverlayIcon = overlayIcon;
+            return this;
+        }
+
+        /**
+         * Returns the sub-text for {@link Notification#mLargeIcon}.
+         * @see EnRouteStyle#setLargeIconSubText
+         */
+        @Nullable
+        public CharSequence getLargeIconSubText() {
+            return mLargeIconSubText;
+        }
+
+        /**
+         * Optional text which generally related to
+         * the {@link Notification.Builder#setLargeIcon} or {@link #setOverlayIcon} or both.
+         */
+        @NonNull
+        public EnRouteStyle setLargeIconSubText(@Nullable CharSequence largeIconSubText) {
+            mLargeIconSubText = stripStyling(largeIconSubText);
+            return this;
+        }
+
+         /**
+         * @hide
+         */
+        @Override
+        public boolean areNotificationsVisiblyDifferent(Style other) {
+            if (other == null || getClass() != other.getClass()) {
+                return true;
+            }
+
+            final EnRouteStyle enRouteStyle = (EnRouteStyle) other;
+            return !Objects.equals(mOverlayIcon, enRouteStyle.mOverlayIcon)
+                    || !Objects.equals(mLargeIconSubText, enRouteStyle.mLargeIconSubText);
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void addExtras(Bundle extras) {
+            super.addExtras(extras);
+            extras.putParcelable(EXTRA_ENROUTE_OVERLAY_ICON, mOverlayIcon);
+            extras.putCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT, mLargeIconSubText);
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        protected void restoreFromExtras(Bundle extras) {
+            super.restoreFromExtras(extras);
+            mOverlayIcon = extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class);
+            mLargeIconSubText = extras.getCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT);
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void purgeResources() {
+            super.purgeResources();
+            if (mOverlayIcon != null) {
+                mOverlayIcon.convertToAshmem();
+            }
+        }
+
+        /**
+         * @hide
+         */
+        @Override
+        public void reduceImageSizes(Context context) {
+            super.reduceImageSizes(context);
+            if (mOverlayIcon != null) {
+                final Resources resources = context.getResources();
+                final boolean isLowRam = ActivityManager.isLowRamDeviceStatic();
+
+                int rightIconSize = resources.getDimensionPixelSize(isLowRam
+                        ? R.dimen.notification_right_icon_size_low_ram
+                        : R.dimen.notification_right_icon_size);
+                mOverlayIcon.scaleDownIfNecessary(rightIconSize, rightIconSize);
+            }
+        }
+    }
+
+    /**
      * Notification style for custom views that are decorated by the system
      *
      * <p>Instead of providing a notification that is completely custom, a developer can set this
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 326d7ce..789c99d 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -753,9 +753,14 @@
 
     /**
      * Sets whether or not notifications posted to this channel can interrupt the user in
-     * {@link android.app.NotificationManager.Policy#INTERRUPTION_FILTER_PRIORITY} mode.
+     * {@link android.app.NotificationManager#INTERRUPTION_FILTER_PRIORITY} mode.
      *
-     * Only modifiable by the system and notification ranker.
+     * <p>Apps with Do Not Disturb policy access (see
+     * {@link NotificationManager#isNotificationPolicyAccessGranted()}) can set up their own
+     * channels this way, but only if the channel hasn't been updated by the user since its
+     * creation.
+     *
+     * <p>Otherwise, this value is only modifiable by the system and the notification ranker.
      */
     public void setBypassDnd(boolean bypassDnd) {
         this.mBypassDnd = bypassDnd;
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 3d1a785..83f9ff7 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1440,10 +1440,36 @@
      * Informs the notification manager that the state of an {@link AutomaticZenRule} has changed.
      * Use this method to put the system into Do Not Disturb mode or request that it exits Do Not
      * Disturb mode. The calling app must own the provided {@link android.app.AutomaticZenRule}.
-     * <p>
-     *     This method can be used in conjunction with or as a replacement to
-     *     {@link android.service.notification.ConditionProviderService#notifyCondition(Condition)}.
-     * </p>
+     *
+     * <p>This method can be used in conjunction with or as a replacement to
+     * {@link android.service.notification.ConditionProviderService#notifyCondition(Condition)}.
+     *
+     * <p>The condition change may be ignored if the user has activated or deactivated the rule
+     * manually -- the user can "override" the rule <em>this time</em>, with the rule resuming its
+     * normal operation for the next cycle. When this has happened, the supplied condition will be
+     * applied only once the automatic state is in agreement with the user-provided state. For
+     * example, assume that the {@link AutomaticZenRule} corresponds to a "Driving Mode" with
+     * automatic driving detection.
+     *
+     * <ol>
+     *     <li>App detects driving and notifies the system that the rule should be active, calling
+     *     this method with a {@link Condition} with {@link Condition#STATE_TRUE}).
+     *     <li>User deactivates ("snoozes") the rule for some reason. This overrides the
+     *     app-provided condition state.
+     *     <li>App is still detecting driving, so again calls with {@link Condition#STATE_TRUE}.
+     *     This is ignored by the system, as the user override prevails.
+     *     <li>Some time later, the app detects that driving stopped, so the rule should be
+     *     inactive, and calls with {@link Condition#STATE_FALSE}). This doesn't change the actual
+     *     rule state (it was already inactive due to the user's override), but clears the override.
+     *     <li>Some time later, the app detects that driving has started again, and notifies that
+     *     the rule should be active (calling with {@link Condition#STATE_TRUE} again). The rule is
+     *     activated.
+     * </ol>
+     *
+     * <p>Note that the behavior at step #3 is different if the app also specifies
+     * {@link Condition#SOURCE_USER_ACTION} as the {@link Condition#source} -- rule state updates
+     * coming from user actions are not ignored.
+     *
      * @param id The id of the rule whose state should change
      * @param condition The new state of this rule
      */
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index bf21549..7801201 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -27,7 +27,6 @@
  *
  * <p>App function is a specific piece of functionality that an app offers to the system. These
  * functionalities can be integrated into various system features.
- *
  */
 @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
 @SystemService(Context.APP_FUNCTION_SERVICE)
diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
index 018bc75..14944f0 100644
--- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl
@@ -16,9 +16,22 @@
 
 package android.app.appfunctions;
 
+import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
+import android.app.appfunctions.IExecuteAppFunctionCallback;
+
 /**
 * Interface between an app and the server implementation service (AppFunctionManagerService).
 * @hide
 */
 oneway interface IAppFunctionManager {
+    /**
+    * Executes an app function provided by {@link AppFunctionService} through the system.
+    *
+    * @param request the request to execute an app function.
+    * @param callback the callback to report the result.
+    */
+    void executeAppFunction(
+        in ExecuteAppFunctionAidlRequest request,
+        in IExecuteAppFunctionCallback callback
+    );
 }
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
index 564fb02..7c674f9 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
@@ -18,6 +18,7 @@
 
 import android.content.ComponentName;
 import android.content.IntentSender;
+import android.os.UserHandle;
 
 /**
  * Interface to listen for activity changes in a virtual device.
@@ -48,9 +49,9 @@
      *
      * @param displayId The display ID on which the activity tried to launch.
      * @param componentName The component name of the blocked activity.
-     * @param userId The user ID associated with the blocked activity.
+     * @param user The user associated with the blocked activity.
      * @param intentSender The original sender of the intent.
      */
-    void onActivityLaunchBlocked(int displayId, in ComponentName componentName, int userId,
+    void onActivityLaunchBlocked(int displayId, in ComponentName componentName, in UserHandle user,
             in IntentSender intentSender);
 }
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 19eb497..9636cd4 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -19,7 +19,7 @@
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
-import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
 
@@ -65,6 +65,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.view.WindowManager;
 
@@ -136,14 +137,14 @@
 
                 @Override
                 public void onActivityLaunchBlocked(int displayId, ComponentName componentName,
-                        @UserIdInt int userId, IntentSender intentSender) {
+                        UserHandle user, IntentSender intentSender) {
                     final long token = Binder.clearCallingIdentity();
                     try {
                         synchronized (mActivityListenersLock) {
                             for (int i = 0; i < mActivityListeners.size(); i++) {
                                 mActivityListeners.valueAt(i)
                                         .onActivityLaunchBlocked(
-                                                displayId, componentName, userId, intentSender);
+                                                displayId, componentName, user, intentSender);
                             }
                         }
                     } finally {
@@ -292,7 +293,7 @@
             case POLICY_TYPE_RECENTS:
             case POLICY_TYPE_CLIPBOARD:
             case POLICY_TYPE_ACTIVITY:
-            case POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR:
+            case POLICY_TYPE_BLOCKED_ACTIVITY:
                 break;
             default:
                 throw new IllegalArgumentException("Device policy " + policyType
@@ -595,10 +596,10 @@
         }
 
         public void onActivityLaunchBlocked(int displayId, ComponentName componentName,
-                @UserIdInt int userId, IntentSender intentSender) {
+                UserHandle user, IntentSender intentSender) {
             mExecutor.execute(() ->
                     mActivityListener.onActivityLaunchBlocked(
-                            displayId, componentName, userId, intentSender));
+                            displayId, componentName, user, intentSender));
         }
     }
 
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index d07fb25..c2300e0 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -67,6 +67,7 @@
 import android.os.Binder;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.Log;
 import android.view.Display;
@@ -1263,7 +1264,7 @@
          *
          * @param displayId The display ID on which the activity tried to launch.
          * @param componentName The component name of the blocked activity.
-         * @param userId The user ID associated with the blocked activity.
+         * @param user The user associated with the blocked activity.
          * @param intentSender The original sender of the intent. May be {@code null} if the sender
          *   expects an activity result to be reported. In that case
          *   {@link android.app.Activity#RESULT_CANCELED} was already reported back because the
@@ -1275,7 +1276,7 @@
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
         default void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName,
-                @UserIdInt int userId, @Nullable IntentSender intentSender) {}
+                @NonNull UserHandle user, @Nullable IntentSender intentSender) {}
     }
 
     /**
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index c1fc51d..03b72bd 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -160,7 +160,7 @@
      */
     @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
             POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA,
-            POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR})
+            POLICY_TYPE_BLOCKED_ACTIVITY})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
     public @interface PolicyType {}
@@ -172,7 +172,7 @@
      * @hide
      */
     @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY,
-            POLICY_TYPE_CLIPBOARD, POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR})
+            POLICY_TYPE_CLIPBOARD, POLICY_TYPE_BLOCKED_ACTIVITY})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
     public @interface DynamicPolicyType {}
@@ -242,7 +242,7 @@
      * @see VirtualDeviceManager.VirtualDevice#removeActivityPolicyExemption
      */
     // TODO(b/333443509): Update the documentation of custom policy and link to the new policy
-    // POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR
+    // POLICY_TYPE_BLOCKED_ACTIVITY
     @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
     public static final int POLICY_TYPE_ACTIVITY = 3;
 
@@ -292,7 +292,7 @@
      */
     // TODO(b/333443509): Link to POLICY_TYPE_ACTIVITY
     @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
-    public static final int POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR = 6;
+    public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6;
 
     private final int mLockState;
     @NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
@@ -1206,7 +1206,7 @@
             }
 
             if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
-                mDevicePolicies.delete(POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR);
+                mDevicePolicies.delete(POLICY_TYPE_BLOCKED_ACTIVITY);
             }
 
             if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE
diff --git a/core/java/android/content/rollback/PackageRollbackInfo.java b/core/java/android/content/rollback/PackageRollbackInfo.java
index 8df7c37..21122a1 100644
--- a/core/java/android/content/rollback/PackageRollbackInfo.java
+++ b/core/java/android/content/rollback/PackageRollbackInfo.java
@@ -16,6 +16,7 @@
 
 package android.content.rollback;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.content.pm.PackageManager;
@@ -145,7 +146,10 @@
         mPendingRestores.remove(ri);
     }
 
-    /** @hide */
+    /**
+     * True if the package is an apex else false.
+     */
+    @FlaggedApi(android.crashrecovery.flags.Flags.FLAG_ENABLE_CRASHRECOVERY)
     public boolean isApex() {
         return mIsApex;
     }
@@ -154,7 +158,11 @@
     public @PackageManager.RollbackDataPolicy int getRollbackDataPolicy() {
         return mRollbackDataPolicy;
     }
-    /** @hide */
+
+    /**
+     * True if the package is apk-in-apex else false.
+     */
+    @FlaggedApi(android.crashrecovery.flags.Flags.FLAG_ENABLE_CRASHRECOVERY)
     public boolean isApkInApex() {
         return mIsApkInApex;
     }
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index 1e7f70b..48d2785 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -80,6 +80,7 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -354,7 +355,14 @@
         mCameraId = cameraId;
         if (Flags.singleThreadExecutor()) {
             mDeviceCallback = new ClientStateCallback(executor, callback);
-            mDeviceExecutor = Executors.newSingleThreadExecutor();
+            mDeviceExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+                @Override
+                public Thread newThread(Runnable r) {
+                    Thread thread = Executors.defaultThreadFactory().newThread(r);
+                    thread.setName("CameraDeviceExecutor");
+                    return thread;
+                }
+            });
         } else {
             mDeviceCallback = callback;
             mDeviceExecutor = executor;
@@ -2272,6 +2280,19 @@
                 // TODO: Handle CameraCharacteristics access from CaptureResult correctly.
                 result.set(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE,
                         getCharacteristics().get(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE));
+                Map<String, CameraCharacteristics> physicalIdToChars = getPhysicalIdToChars();
+                for (PhysicalCaptureResultInfo oneResultInfo : physicalResults) {
+                    String physicalId = oneResultInfo.getCameraId();
+                    CameraMetadataNative physicalResult = oneResultInfo.getCameraMetadata();
+                    CameraCharacteristics ch = physicalIdToChars.get(physicalId);
+                    if (ch != null)  {
+                        physicalResult.set(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE,
+                                ch.get(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE));
+                    } else {
+                        Log.e(TAG, "Unable to find characteristics for physical camera "
+                                + physicalId);
+                    }
+                }
 
                 final CaptureCallbackHolder holder =
                         CameraDeviceImpl.this.mCaptureCallbackMap.get(requestId);
diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java
index 623196b..492b825 100644
--- a/core/java/android/os/BatteryConsumer.java
+++ b/core/java/android/os/BatteryConsumer.java
@@ -71,7 +71,7 @@
             POWER_COMPONENT_REATTRIBUTED_TO_OTHER_CONSUMERS,
     })
     @Retention(RetentionPolicy.SOURCE)
-    public static @interface PowerComponent {
+    public @interface PowerComponent {
     }
 
     public static final int POWER_COMPONENT_ANY = -1;
@@ -132,6 +132,16 @@
     }
 
     /**
+     * An integer that is either one of @PowerComponent constants or a custom component ID
+     * between FIRST_CUSTOM_POWER_COMPONENT_ID and LAST_CUSTOM_POWER_COMPONENT_ID.
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PowerComponentId {
+    }
+
+    /**
      * Identifiers of models used for power estimation.
      *
      * @hide
@@ -178,8 +188,8 @@
     public @interface ProcessState {
     }
 
+    public static final int PROCESS_STATE_ANY = -1;
     public static final int PROCESS_STATE_UNSPECIFIED = 0;
-    public static final int PROCESS_STATE_ANY = PROCESS_STATE_UNSPECIFIED;
     public static final int PROCESS_STATE_FOREGROUND = 1;
     public static final int PROCESS_STATE_BACKGROUND = 2;
     public static final int PROCESS_STATE_FOREGROUND_SERVICE = 3;
@@ -216,16 +226,14 @@
     };
 
     static final int COLUMN_INDEX_BATTERY_CONSUMER_TYPE = 0;
-    static final int COLUMN_COUNT = 1;
-
     /**
      * Identifiers of consumed power aggregations per SCREEN state.
      *
      * @hide
      */
     @IntDef(prefix = {"SCREEN_STATE_"}, value = {
-            SCREEN_STATE_UNSPECIFIED,
             SCREEN_STATE_ANY,
+            SCREEN_STATE_UNSPECIFIED,
             SCREEN_STATE_ON,
             SCREEN_STATE_OTHER,
     })
@@ -233,8 +241,10 @@
     public @interface ScreenState {
     }
 
+    static final int COLUMN_COUNT = 1;
+
+    public static final int SCREEN_STATE_ANY = 0;
     public static final int SCREEN_STATE_UNSPECIFIED = 0;
-    public static final int SCREEN_STATE_ANY = SCREEN_STATE_UNSPECIFIED;
     public static final int SCREEN_STATE_ON = 1;
     public static final int SCREEN_STATE_OTHER = 2;  // Off, doze etc
 
@@ -255,8 +265,8 @@
      * @hide
      */
     @IntDef(prefix = {"POWER_STATE_"}, value = {
-            POWER_STATE_UNSPECIFIED,
             POWER_STATE_ANY,
+            POWER_STATE_UNSPECIFIED,
             POWER_STATE_BATTERY,
             POWER_STATE_OTHER,
     })
@@ -264,8 +274,8 @@
     public @interface PowerState {
     }
 
+    public static final int POWER_STATE_ANY = 0;
     public static final int POWER_STATE_UNSPECIFIED = 0;
-    public static final int POWER_STATE_ANY = POWER_STATE_UNSPECIFIED;
     public static final int POWER_STATE_BATTERY = 1;
     public static final int POWER_STATE_OTHER = 2;   // Plugged in, or on wireless charger, etc.
 
@@ -284,18 +294,18 @@
      * Identifies power attribution dimensions that a caller is interested in.
      */
     public static final class Dimensions {
-        public final @PowerComponent int powerComponent;
+        public final @PowerComponentId int powerComponentId;
         public final @ProcessState int processState;
         public final @ScreenState int screenState;
         public final @PowerState int powerState;
 
-        public Dimensions(@PowerComponent int powerComponent, @ProcessState int processState) {
-            this(powerComponent, processState, SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED);
+        public Dimensions(@PowerComponentId int powerComponentId, @ProcessState int processState) {
+            this(powerComponentId, processState, SCREEN_STATE_ANY, POWER_STATE_ANY);
         }
 
-        public Dimensions(@PowerComponent int powerComponent, int processState,
+        public Dimensions(@PowerComponentId int powerComponentId, int processState,
                 @ScreenState int screenState, @PowerState int powerState) {
-            this.powerComponent = powerComponent;
+            this.powerComponentId = powerComponentId;
             this.processState = processState;
             this.screenState = screenState;
             this.powerState = powerState;
@@ -305,11 +315,16 @@
         public String toString() {
             boolean dimensionSpecified = false;
             StringBuilder sb = new StringBuilder();
-            if (powerComponent != POWER_COMPONENT_ANY) {
-                sb.append("powerComponent=").append(sPowerComponentNames[powerComponent]);
+            if (powerComponentId != POWER_COMPONENT_ANY) {
+                sb.append("powerComponent=");
+                if (powerComponentId < POWER_COMPONENT_COUNT) {
+                    sb.append(sPowerComponentNames[powerComponentId]);
+                } else {
+                    sb.append("CUSTOM/").append(powerComponentId);
+                }
                 dimensionSpecified = true;
             }
-            if (processState != PROCESS_STATE_UNSPECIFIED) {
+            if (processState != PROCESS_STATE_ANY) {
                 if (dimensionSpecified) {
                     sb.append(", ");
                 }
@@ -353,7 +368,7 @@
      * in the same BatteryUsageStats.
      */
     public static final class Key {
-        public final @PowerComponent int powerComponent;
+        public final @PowerComponentId int powerComponentId;
         public final @ProcessState int processState;
         public final @ScreenState int screenState;
         public final @PowerState int powerState;
@@ -362,10 +377,10 @@
         final int mPowerColumnIndex;
         final int mDurationColumnIndex;
 
-        private Key(@PowerComponent int powerComponent, @ProcessState int processState,
+        private Key(@PowerComponentId int powerComponentId, @ProcessState int processState,
                 @ScreenState int screenState, @PowerState int powerState, int powerModelColumnIndex,
                 int powerColumnIndex, int durationColumnIndex) {
-            this.powerComponent = powerComponent;
+            this.powerComponentId = powerComponentId;
             this.processState = processState;
             this.screenState = screenState;
             this.powerState = powerState;
@@ -379,9 +394,13 @@
          * Returns true if this key should be included in an enumeration parameterized with
          * the supplied dimensions.
          */
-        boolean matches(@PowerComponent int powerComponent, @ProcessState int processState,
+        boolean matches(@PowerComponentId int powerComponent, @ProcessState int processState,
                 @ScreenState int screenState, @PowerState int powerState) {
-            if (powerComponent != POWER_COMPONENT_ANY && this.powerComponent != powerComponent) {
+            if (powerComponent != POWER_COMPONENT_ANY && this.powerComponentId != powerComponent) {
+                return false;
+            }
+            if (this.processState == PROCESS_STATE_UNSPECIFIED) {
+                // PROCESS_STATE_UNSPECIFIED is used for storing a precomputed total
                 return false;
             }
             if (processState != PROCESS_STATE_ANY && this.processState != processState) {
@@ -401,7 +420,7 @@
         public boolean equals(Object o) {
             // Skipping null and class check for performance
             final Key key = (Key) o;
-            return powerComponent == key.powerComponent
+            return powerComponentId == key.powerComponentId
                     && processState == key.processState
                     && screenState == key.screenState
                     && powerState == key.powerState;
@@ -409,7 +428,7 @@
 
         @Override
         public int hashCode() {
-            int result = powerComponent;
+            int result = powerComponentId;
             result = 31 * result + processState;
             result = 31 * result + screenState;
             result = 31 * result + powerState;
@@ -419,11 +438,15 @@
         /**
          * Returns a string suitable for use in dumpsys.
          */
-        public static String toString(@PowerComponent int powerComponent,
+        public static String toString(@PowerComponentId int powerComponent,
                 @ProcessState int processState, @ScreenState int screenState,
                 @PowerState int powerState) {
             StringBuilder sb = new StringBuilder();
-            sb.append(powerComponentIdToString(powerComponent));
+            if (powerComponent < POWER_COMPONENT_COUNT) {
+                sb.append(powerComponentIdToString(powerComponent));
+            } else {
+                sb.append("CUSTOM/").append(powerComponent);
+            }
             if (processState != PROCESS_STATE_UNSPECIFIED) {
                 sb.append(':');
                 sb.append(processStateToString(processState));
@@ -441,7 +464,7 @@
 
         @Override
         public String toString() {
-            return toString(powerComponent, processState, screenState, powerState);
+            return toString(powerComponentId, processState, screenState, powerState);
         }
     }
 
@@ -459,6 +482,13 @@
     }
 
     /**
+     * Returns the name of the specified power component, e.g. "CPU", "GPU" etc.
+     */
+    public String getPowerComponentName(@PowerComponentId int powerComponent) {
+        return mData.layout.getPowerComponentName(powerComponent);
+    }
+
+    /**
      * Total power consumed by this consumer, in mAh.
      */
     public double getConsumedPower() {
@@ -480,10 +510,18 @@
     }
 
     /**
+     * Returns indexes of all included power components.
+     */
+    @PowerComponentId
+    public int[] getPowerComponentIds() {
+        return mData.layout.powerComponentIds;
+    }
+
+    /**
      * Returns keys for various power values attributed to the specified component
      * held by this BatteryUsageStats object.
      */
-    public Key[] getKeys(@PowerComponent int componentId) {
+    public Key[] getKeys(@PowerComponentId int componentId) {
         return mData.layout.getKeys(componentId);
     }
 
@@ -491,7 +529,7 @@
      * Returns the key for the power attributed to the specified component,
      * for all values of other dimensions such as process state.
      */
-    public Key getKey(@PowerComponent int componentId) {
+    public Key getKey(@PowerComponentId int componentId) {
         return mData.layout.getKey(componentId, PROCESS_STATE_UNSPECIFIED, SCREEN_STATE_UNSPECIFIED,
                 POWER_STATE_UNSPECIFIED);
     }
@@ -499,7 +537,7 @@
     /**
      * Returns the key for the power attributed to the specified component and process state.
      */
-    public Key getKey(@PowerComponent int componentId, @ProcessState int processState) {
+    public Key getKey(@PowerComponentId int componentId, @ProcessState int processState) {
         return mData.layout.getKey(componentId, processState, SCREEN_STATE_UNSPECIFIED,
                 POWER_STATE_UNSPECIFIED);
     }
@@ -511,9 +549,9 @@
      *                    {@link BatteryConsumer#POWER_COMPONENT_CPU}.
      * @return Amount of consumed power in mAh.
      */
-    public double getConsumedPower(@PowerComponent int componentId) {
-        return mPowerComponents.getConsumedPower(componentId, PROCESS_STATE_UNSPECIFIED,
-                        SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED);
+    public double getConsumedPower(@PowerComponentId int componentId) {
+        return mPowerComponents.getConsumedPower(componentId, PROCESS_STATE_ANY,
+                        SCREEN_STATE_ANY, POWER_STATE_ANY);
     }
 
     /**
@@ -533,7 +571,7 @@
      * @param componentId The ID of the power component, e.g.
      *                    {@link BatteryConsumer#POWER_COMPONENT_CPU}.
      */
-    public @PowerModel int getPowerModel(@BatteryConsumer.PowerComponent int componentId) {
+    public @PowerModel int getPowerModel(@PowerComponentId int componentId) {
         return mPowerComponents.getPowerModel(
                 mData.layout.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED,
                         SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED));
@@ -554,9 +592,12 @@
      *
      * @param componentId The ID of the custom power component.
      * @return Amount of consumed power in mAh.
+     *
+     * @deprecated Use getConsumedPower instead
      */
+    @Deprecated
     public double getConsumedPowerForCustomComponent(int componentId) {
-        return mPowerComponents.getConsumedPowerForCustomComponent(componentId);
+        return getConsumedPower(componentId);
     }
 
     public int getCustomPowerComponentCount() {
@@ -580,8 +621,9 @@
      *                    {@link UidBatteryConsumer#POWER_COMPONENT_CPU}.
      * @return Amount of time in milliseconds.
      */
-    public long getUsageDurationMillis(@PowerComponent int componentId) {
-        return mPowerComponents.getUsageDurationMillis(getKey(componentId));
+    public long getUsageDurationMillis(@PowerComponentId int componentId) {
+        return mPowerComponents.getUsageDurationMillis(componentId, PROCESS_STATE_ANY,
+                SCREEN_STATE_ANY, POWER_STATE_ANY);
     }
 
     /**
@@ -598,17 +640,6 @@
     }
 
     /**
-     * Returns the amount of usage time attributed to the specified custom component
-     * since BatteryStats reset.
-     *
-     * @param componentId The ID of the custom power component.
-     * @return Amount of time in milliseconds.
-     */
-    public long getUsageDurationForCustomComponentMillis(int componentId) {
-        return mPowerComponents.getUsageDurationForCustomComponentMillis(componentId);
-    }
-
-    /**
      * Returns the name of the specified component.  Intended for logging and debugging.
      */
     public static String powerComponentIdToString(@BatteryConsumer.PowerComponent int componentId) {
@@ -826,13 +857,12 @@
         public final boolean processStateDataIncluded;
         public final boolean screenStateDataIncluded;
         public final boolean powerStateDataIncluded;
+        public final @PowerComponentId int[] powerComponentIds;
         public final Key[] keys;
         public final SparseArray<Key> indexedKeys;
         public final int totalConsumedPowerColumnIndex;
-        public final int firstCustomConsumedPowerColumn;
-        public final int firstCustomUsageDurationColumn;
         public final int columnCount;
-        private Key[][] mPerComponentKeys;
+        private SparseArray<Key[]> mPerComponentKeys;
 
         private BatteryConsumerDataLayout(int firstColumn, String[] customPowerComponentNames,
                 boolean powerModelsIncluded, boolean includeProcessStateData,
@@ -844,6 +874,15 @@
             this.screenStateDataIncluded = includeScreenState;
             this.powerStateDataIncluded = includePowerState;
 
+            powerComponentIds = new int[POWER_COMPONENT_COUNT + customPowerComponentCount];
+            int id = 0;
+            for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) {
+                powerComponentIds[id++] = componentId;
+            }
+            for (int i = 0; i < customPowerComponentCount; i++) {
+                powerComponentIds[id++] = FIRST_CUSTOM_POWER_COMPONENT_ID + i;
+            }
+
             int columnIndex = firstColumn;
 
             totalConsumedPowerColumnIndex = columnIndex++;
@@ -857,35 +896,41 @@
                     if (!includePowerState && powerState != POWER_STATE_UNSPECIFIED) {
                         continue;
                     }
-                    for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) {
+                    for (int i = 0; i < powerComponentIds.length; i++) {
                         columnIndex = addKeys(keyList, powerModelsIncluded, includeProcessStateData,
-                                componentId, screenState, powerState, columnIndex);
+                                powerComponentIds[i], screenState, powerState, columnIndex);
                     }
                 }
             }
 
-            firstCustomConsumedPowerColumn = columnIndex;
-            columnIndex += customPowerComponentCount;
-
-            firstCustomUsageDurationColumn = columnIndex;
-            columnIndex += customPowerComponentCount;
-
             columnCount = columnIndex;
 
             keys = keyList.toArray(KEY_ARRAY);
             indexedKeys = new SparseArray<>(keys.length);
             for (int i = 0; i < keys.length; i++) {
                 Key key = keys[i];
-                int index = keyIndex(key.powerComponent, key.processState, key.screenState,
-                        key.powerState);
-                indexedKeys.put(index, key);
+                indexedKeys.put(keyIndex(key.powerComponentId, key.processState, key.screenState,
+                        key.powerState), key);
+            }
+        }
+
+        public String getPowerComponentName(@PowerComponentId int powerComponentId) {
+            if (powerComponentId < POWER_COMPONENT_COUNT) {
+                return BatteryConsumer.powerComponentIdToString(powerComponentId);
+            } else if (powerComponentId >= FIRST_CUSTOM_POWER_COMPONENT_ID && powerComponentId
+                    < FIRST_CUSTOM_POWER_COMPONENT_ID + customPowerComponentCount) {
+                return customPowerComponentNames[powerComponentId
+                        - FIRST_CUSTOM_POWER_COMPONENT_ID];
+            } else {
+                throw new IllegalArgumentException(
+                        "Unsupported power component " + powerComponentId);
             }
         }
 
         private int addKeys(List<Key> keys, boolean powerModelsIncluded,
-                boolean includeProcessStateData, int componentId,
+                boolean includeProcessStateData, @PowerComponentId int componentId,
                 int screenState, int powerState, int columnIndex) {
-            keys.add(new Key(componentId, PROCESS_STATE_ANY, screenState, powerState,
+            keys.add(new Key(componentId, PROCESS_STATE_UNSPECIFIED, screenState, powerState,
                     powerModelsIncluded
                             ? columnIndex++
                             : POWER_MODEL_NOT_INCLUDED,  // power model
@@ -896,14 +941,13 @@
             // Declare Keys for all process states, if needed
             if (includeProcessStateData) {
                 boolean isSupported = SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE
-                        .binarySearch(componentId) >= 0;
+                        .binarySearch(componentId) >= 0
+                        || componentId >= FIRST_CUSTOM_POWER_COMPONENT_ID;
                 if (isSupported) {
-                    for (int processState = 0; processState < PROCESS_STATE_COUNT;
-                            processState++) {
-                        if (processState == PROCESS_STATE_UNSPECIFIED) {
+                    for (int processState = 0; processState < PROCESS_STATE_COUNT; processState++) {
+                        if (processState == PROCESS_STATE_UNSPECIFIED) { // Already added above
                             continue;
                         }
-
                         keys.add(new Key(componentId, processState, screenState, powerState,
                                 powerModelsIncluded
                                         ? columnIndex++
@@ -917,12 +961,12 @@
             return columnIndex;
         }
 
-        Key getKey(@PowerComponent int componentId, @ProcessState int processState,
+        Key getKey(@PowerComponentId int componentId, @ProcessState int processState,
                 @ScreenState int screenState, @PowerState int powerState) {
             return indexedKeys.get(keyIndex(componentId, processState, screenState, powerState));
         }
 
-        Key getKeyOrThrow(@PowerComponent int componentId, @ProcessState int processState,
+        Key getKeyOrThrow(@PowerComponentId int componentId, @ProcessState int processState,
                 @ScreenState int screenState, @PowerState int powerState) {
             Key key = getKey(componentId, processState, screenState, powerState);
             if (key == null) {
@@ -933,21 +977,21 @@
             return key;
         }
 
-        public Key[] getKeys(@PowerComponent int componentId) {
+        public Key[] getKeys(@PowerComponentId int componentId) {
             synchronized (this) {
                 if (mPerComponentKeys == null) {
-                    mPerComponentKeys = new Key[BatteryConsumer.POWER_COMPONENT_COUNT][];
+                    mPerComponentKeys = new SparseArray<>(powerComponentIds.length);
                 }
-                Key[] componentKeys = mPerComponentKeys[componentId];
+                Key[] componentKeys = mPerComponentKeys.get(componentId);
                 if (componentKeys == null) {
                     ArrayList<Key> out = new ArrayList<>();
                     for (Key key : keys) {
-                        if (key.powerComponent == componentId) {
+                        if (key.powerComponentId == componentId) {
                             out.add(key);
                         }
                     }
                     componentKeys = out.toArray(new Key[out.size()]);
-                    mPerComponentKeys[componentId] = componentKeys;
+                    mPerComponentKeys.put(componentId, componentKeys);
                 }
                 return componentKeys;
             }
@@ -991,18 +1035,18 @@
         }
 
         @Nullable
-        public Key[] getKeys(@PowerComponent int componentId) {
+        public Key[] getKeys(@PowerComponentId int componentId) {
             return mData.layout.getKeys(componentId);
         }
 
         @Nullable
-        public Key getKey(@PowerComponent int componentId, @ProcessState int processState) {
+        public Key getKey(@PowerComponentId int componentId, @ProcessState int processState) {
             return mData.layout.getKey(componentId, processState, SCREEN_STATE_UNSPECIFIED,
                     POWER_STATE_UNSPECIFIED);
         }
 
         @Nullable
-        public Key getKey(@PowerComponent int componentId, @ProcessState int processState,
+        public Key getKey(@PowerComponentId int componentId, @ProcessState int processState,
                 @ScreenState int screenState, @PowerState int powerState) {
             return mData.layout.getKey(componentId, processState, screenState, powerState);
         }
@@ -1015,7 +1059,7 @@
          * @param componentPower Amount of consumed power in mAh.
          */
         @NonNull
-        public T setConsumedPower(@PowerComponent int componentId, double componentPower) {
+        public T setConsumedPower(@PowerComponentId int componentId, double componentPower) {
             return setConsumedPower(componentId, componentPower, POWER_MODEL_POWER_PROFILE);
         }
 
@@ -1028,7 +1072,7 @@
          */
         @SuppressWarnings("unchecked")
         @NonNull
-        public T setConsumedPower(@PowerComponent int componentId, double componentPower,
+        public T setConsumedPower(@PowerComponentId int componentId, double componentPower,
                 @PowerModel int powerModel) {
             mPowerComponentsBuilder.setConsumedPower(getKey(componentId, PROCESS_STATE_UNSPECIFIED),
                     componentPower, powerModel);
@@ -1037,7 +1081,7 @@
 
         @SuppressWarnings("unchecked")
         @NonNull
-        public T addConsumedPower(@PowerComponent int componentId, double componentPower,
+        public T addConsumedPower(@PowerComponentId int componentId, double componentPower,
                 @PowerModel int powerModel) {
             mPowerComponentsBuilder.addConsumedPower(getKey(componentId, PROCESS_STATE_UNSPECIFIED),
                     componentPower, powerModel);
@@ -1059,26 +1103,6 @@
         }
 
         /**
-         * Sets the amount of drain attributed to the specified custom drain type.
-         *
-         * @param componentId    The ID of the custom power component.
-         * @param componentPower Amount of consumed power in mAh.
-         */
-        @SuppressWarnings("unchecked")
-        @NonNull
-        public T setConsumedPowerForCustomComponent(int componentId, double componentPower) {
-            mPowerComponentsBuilder.setConsumedPowerForCustomComponent(componentId, componentPower);
-            return (T) this;
-        }
-
-        @SuppressWarnings("unchecked")
-        @NonNull
-        public T addConsumedPowerForCustomComponent(int componentId, double componentPower) {
-            mPowerComponentsBuilder.addConsumedPowerForCustomComponent(componentId, componentPower);
-            return (T) this;
-        }
-
-        /**
          * Sets the amount of time used by the specified component, e.g. CPU, WiFi etc.
          *
          * @param componentId              The ID of the power component, e.g.
@@ -1087,7 +1111,7 @@
          */
         @SuppressWarnings("unchecked")
         @NonNull
-        public T setUsageDurationMillis(@UidBatteryConsumer.PowerComponent int componentId,
+        public T setUsageDurationMillis(@PowerComponentId int componentId,
                 long componentUsageTimeMillis) {
             mPowerComponentsBuilder
                     .setUsageDurationMillis(getKey(componentId, PROCESS_STATE_UNSPECIFIED),
@@ -1095,7 +1119,6 @@
             return (T) this;
         }
 
-
         @SuppressWarnings("unchecked")
         @NonNull
         public T setUsageDurationMillis(Key key, long componentUsageTimeMillis) {
@@ -1104,21 +1127,6 @@
         }
 
         /**
-         * Sets the amount of time used by the specified custom component.
-         *
-         * @param componentId              The ID of the custom power component.
-         * @param componentUsageTimeMillis Amount of time in microseconds.
-         */
-        @SuppressWarnings("unchecked")
-        @NonNull
-        public T setUsageDurationForCustomComponentMillis(int componentId,
-                long componentUsageTimeMillis) {
-            mPowerComponentsBuilder.setUsageDurationForCustomComponentMillis(componentId,
-                    componentUsageTimeMillis);
-            return (T) this;
-        }
-
-        /**
          * Returns the total power accumulated by this builder so far. It may change
          * by the time the {@code build()} method is called.
          */
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index e039953..1fef602 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -95,7 +95,6 @@
     static final String XML_TAG_USER = "user";
     static final String XML_TAG_POWER_COMPONENTS = "power_components";
     static final String XML_TAG_COMPONENT = "component";
-    static final String XML_TAG_CUSTOM_COMPONENT = "custom_component";
     static final String XML_ATTR_ID = "id";
     static final String XML_ATTR_UID = "uid";
     static final String XML_ATTR_USER_ID = "user_id";
@@ -610,96 +609,109 @@
         final BatteryConsumer appsConsumer = getAggregateBatteryConsumer(
                 AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS);
 
-        for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
-                componentId++) {
-            final double devicePowerMah = deviceConsumer.getConsumedPower(componentId);
-            final double appsPowerMah = appsConsumer.getConsumedPower(componentId);
+        for (@BatteryConsumer.PowerComponentId int powerComponent :
+                mBatteryConsumerDataLayout.powerComponentIds) {
+            final double devicePowerMah = deviceConsumer.getConsumedPower(powerComponent);
+            final double appsPowerMah = appsConsumer.getConsumedPower(powerComponent);
             if (devicePowerMah == 0 && appsPowerMah == 0) {
                 continue;
             }
 
-            printPowerComponent(pw, prefix, BatteryConsumer.powerComponentIdToString(componentId),
-                    devicePowerMah, appsPowerMah,
-                    BatteryConsumer.POWER_MODEL_UNDEFINED,
-                    deviceConsumer.getUsageDurationMillis(componentId));
+            printPowerComponent(pw, prefix,
+                    mBatteryConsumerDataLayout.getPowerComponentName(powerComponent),
+                    devicePowerMah, appsPowerMah, BatteryConsumer.POWER_MODEL_UNDEFINED,
+                    deviceConsumer.getUsageDurationMillis(powerComponent));
         }
 
-        for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-                componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                        + mCustomPowerComponentNames.length;
-                componentId++) {
-            final double devicePowerMah =
-                    deviceConsumer.getConsumedPowerForCustomComponent(componentId);
-            final double appsPowerMah =
-                    appsConsumer.getConsumedPowerForCustomComponent(componentId);
-            if (devicePowerMah == 0 && appsPowerMah == 0) {
-                continue;
+        String prefixPlus = prefix + "  ";
+        if (mIncludesPowerStateData && !mIncludesScreenStateData) {
+            for (@BatteryConsumer.PowerState int powerState = 0;
+                    powerState < BatteryConsumer.POWER_STATE_COUNT;
+                    powerState++) {
+                if (powerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+                    dumpPowerComponents(pw, BatteryConsumer.SCREEN_STATE_ANY, powerState,
+                            prefixPlus);
+                }
             }
-
-            printPowerComponent(pw, prefix, deviceConsumer.getCustomPowerComponentName(componentId),
-                    devicePowerMah, appsPowerMah,
-                    BatteryConsumer.POWER_MODEL_UNDEFINED,
-                    deviceConsumer.getUsageDurationForCustomComponentMillis(componentId));
-        }
-
-        if (mIncludesScreenStateData || mIncludesPowerStateData) {
-            String prefixPlus = prefix + "  ";
-            StringBuilder stateLabel = new StringBuilder();
-            int screenState = BatteryConsumer.SCREEN_STATE_UNSPECIFIED;
-            int powerState = BatteryConsumer.POWER_STATE_UNSPECIFIED;
-            for (BatteryConsumer.Key key : mBatteryConsumerDataLayout.keys) {
-                if (key.processState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
-                    continue;
+        } else if (!mIncludesPowerStateData && mIncludesScreenStateData) {
+            for (@BatteryConsumer.ScreenState int screenState = 0;
+                    screenState < BatteryConsumer.SCREEN_STATE_COUNT;
+                    screenState++) {
+                if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) {
+                    dumpPowerComponents(pw, screenState, BatteryConsumer.POWER_STATE_ANY,
+                            prefixPlus);
                 }
-
-                if (key.screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED
-                        && key.powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) {
-                    // Totals already printed earlier in this method
-                    continue;
-                }
-
-                final double devicePowerMah = deviceConsumer.getConsumedPower(key);
-                final double appsPowerMah = appsConsumer.getConsumedPower(key);
-                if (devicePowerMah == 0 && appsPowerMah == 0) {
-                    continue;
-                }
-
-                if (key.screenState != screenState || key.powerState != powerState) {
-                    screenState = key.screenState;
-                    powerState = key.powerState;
-
-                    boolean empty = true;
-                    stateLabel.setLength(0);
-                    stateLabel.append("      (");
-                    if (powerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) {
-                        stateLabel.append(BatteryConsumer.powerStateToString(powerState));
-                        empty = false;
-                    }
-                    if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) {
-                        if (!empty) {
-                            stateLabel.append(", ");
+            }
+        } else if (mIncludesPowerStateData && mIncludesScreenStateData) {
+            for (@BatteryConsumer.PowerState int powerState = 0;
+                    powerState < BatteryConsumer.POWER_STATE_COUNT;
+                    powerState++) {
+                if (powerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+                    for (@BatteryConsumer.ScreenState int screenState = 0;
+                            screenState < BatteryConsumer.SCREEN_STATE_COUNT; screenState++) {
+                        if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) {
+                            dumpPowerComponents(pw, screenState, powerState, prefixPlus);
                         }
-                        stateLabel.append("screen ").append(
-                                BatteryConsumer.screenStateToString(screenState));
-                        empty = false;
-                    }
-                    if (!empty) {
-                        stateLabel.append(")");
-                        pw.println(stateLabel);
                     }
                 }
-                String label = BatteryConsumer.powerComponentIdToString(key.powerComponent);
-                printPowerComponent(pw, prefixPlus, label, devicePowerMah, appsPowerMah,
-                        mIncludesPowerModels ? deviceConsumer.getPowerModel(key)
-                                : BatteryConsumer.POWER_MODEL_UNDEFINED,
-                        deviceConsumer.getUsageDurationMillis(key));
             }
         }
+
         dumpSortedBatteryConsumers(pw, prefix, getUidBatteryConsumers());
         dumpSortedBatteryConsumers(pw, prefix, getUserBatteryConsumers());
         pw.println();
     }
 
+    private void dumpPowerComponents(PrintWriter pw,
+            @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState, String prefix) {
+        final BatteryConsumer deviceConsumer = getAggregateBatteryConsumer(
+                AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
+        final BatteryConsumer appsConsumer = getAggregateBatteryConsumer(
+                AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS);
+
+        boolean labelPrinted = false;
+        for (@BatteryConsumer.PowerComponentId int powerComponent :
+                mBatteryConsumerDataLayout.powerComponentIds) {
+            BatteryConsumer.Dimensions dimensions = new BatteryConsumer.Dimensions(
+                    powerComponent, BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState);
+            final double devicePowerMah = deviceConsumer.getConsumedPower(dimensions);
+            final double appsPowerMah = appsConsumer.getConsumedPower(dimensions);
+            if (devicePowerMah == 0 && appsPowerMah == 0) {
+                continue;
+            }
+
+            if (!labelPrinted) {
+                boolean empty = true;
+                StringBuilder stateLabel = new StringBuilder();
+                stateLabel.append("      (");
+                if (powerState != BatteryConsumer.POWER_STATE_ANY) {
+                    stateLabel.append(BatteryConsumer.powerStateToString(powerState));
+                    empty = false;
+                }
+                if (screenState != BatteryConsumer.SCREEN_STATE_ANY) {
+                    if (!empty) {
+                        stateLabel.append(", ");
+                    }
+                    stateLabel.append("screen ")
+                            .append(BatteryConsumer.screenStateToString(screenState));
+                    empty = false;
+                }
+                if (!empty) {
+                    stateLabel.append(")");
+                    pw.println(stateLabel);
+                    labelPrinted = true;
+                }
+            }
+            printPowerComponent(pw, prefix,
+                    mBatteryConsumerDataLayout.getPowerComponentName(powerComponent),
+                    devicePowerMah, appsPowerMah,
+                    mIncludesPowerModels ? deviceConsumer.getPowerModel(powerComponent)
+                            : BatteryConsumer.POWER_MODEL_UNDEFINED,
+                    deviceConsumer.getUsageDurationMillis(dimensions));
+        }
+    }
+
     private void printPowerComponent(PrintWriter pw, String prefix, String label,
             double devicePowerMah, double appsPowerMah, int powerModel, long durationMs) {
         StringBuilder sb = new StringBuilder();
@@ -951,12 +963,14 @@
 
         /**
          * Returns true if this Builder is configured to hold data for the specified
-         * custom power component ID.
+         * power component index.
          */
-        public boolean isSupportedCustomPowerComponent(int componentId) {
-            return componentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
+        public boolean isSupportedPowerComponent(
+                @BatteryConsumer.PowerComponentId int componentId) {
+            return componentId < BatteryConsumer.POWER_COMPONENT_COUNT
+                    || (componentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
                     && componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                    + mBatteryConsumerDataLayout.customPowerComponentCount;
+                    + mBatteryConsumerDataLayout.customPowerComponentCount);
         }
 
         /**
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index d0ed297..a12606b 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -86,7 +86,7 @@
     private final long mFromTimestamp;
     private final long mToTimestamp;
     private final double mMinConsumedPowerThreshold;
-    private final @BatteryConsumer.PowerComponent int[] mPowerComponents;
+    private final @BatteryConsumer.PowerComponentId int[] mPowerComponents;
 
     private BatteryUsageStatsQuery(@NonNull Builder builder) {
         mFlags = builder.mFlags;
@@ -139,6 +139,7 @@
      * Returns the power components that should be estimated or null if all power components
      * are being requested.
      */
+    @BatteryConsumer.PowerComponentId
     public int[] getPowerComponents() {
         return mPowerComponents;
     }
@@ -228,7 +229,7 @@
         private long mFromTimestamp;
         private long mToTimestamp;
         private double mMinConsumedPowerThreshold = 0;
-        private @BatteryConsumer.PowerComponent int[] mPowerComponents;
+        private @BatteryConsumer.PowerComponentId int[] mPowerComponents;
 
         /**
          * Builds a read-only BatteryUsageStatsQuery object.
@@ -294,7 +295,7 @@
          * is all power components.
          */
         public Builder includePowerComponents(
-                @BatteryConsumer.PowerComponent int[] powerComponents) {
+                @BatteryConsumer.PowerComponentId int[] powerComponents) {
             mPowerComponents = powerComponents;
             return this;
         }
diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java
index f22e1ea..9200db3 100644
--- a/core/java/android/os/PowerComponents.java
+++ b/core/java/android/os/PowerComponents.java
@@ -60,14 +60,14 @@
      * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh.
      */
     public double getConsumedPower(@NonNull BatteryConsumer.Dimensions dimensions) {
-        return getConsumedPower(dimensions.powerComponent, dimensions.processState,
+        return getConsumedPower(dimensions.powerComponentId, dimensions.processState,
                 dimensions.screenState, dimensions.powerState);
     }
 
     /**
      * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh.
      */
-    public double getConsumedPower(@BatteryConsumer.PowerComponent int powerComponent,
+    public double getConsumedPower(@BatteryConsumer.PowerComponentId int powerComponent,
             @BatteryConsumer.ProcessState int processState,
             @BatteryConsumer.ScreenState int screenState,
             @BatteryConsumer.PowerState int powerState) {
@@ -76,85 +76,64 @@
             return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex);
         }
 
-        if (powerComponent != POWER_COMPONENT_ANY
-                && ((mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY)
-                || (mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY))) {
-            BatteryConsumer.Key key = mData.layout.getKey(powerComponent,
-                    processState, screenState, powerState);
-            if (key != null) {
-                return mData.getDouble(key.mPowerColumnIndex);
-            }
+        if (!mData.layout.processStateDataIncluded && !(processState == PROCESS_STATE_UNSPECIFIED
+                || processState == PROCESS_STATE_ANY)) {
             return 0;
         }
 
-        if (mData.layout.processStateDataIncluded || mData.layout.screenStateDataIncluded
-                || mData.layout.powerStateDataIncluded) {
-            double total = 0;
-            for (BatteryConsumer.Key key : mData.layout.keys) {
-                if (key.processState != PROCESS_STATE_UNSPECIFIED
-                        && key.matches(powerComponent, processState, screenState, powerState)) {
-                    total += mData.getDouble(key.mPowerColumnIndex);
-                }
-            }
-            if (total != 0) {
-                return total;
-            }
-        }
-
-        BatteryConsumer.Key key = mData.layout.getKey(powerComponent, processState,
-                SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED);
-        if (key != null) {
+        BatteryConsumer.Key key = mData.layout.getKey(powerComponent,
+                mData.layout.processStateDataIncluded && processState != PROCESS_STATE_ANY
+                        ? processState : PROCESS_STATE_UNSPECIFIED,
+                mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY
+                        ? screenState : SCREEN_STATE_UNSPECIFIED,
+                mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY
+                        ? powerState : POWER_STATE_UNSPECIFIED);
+        if (key != null && mData.hasValue(key.mPowerColumnIndex)) {
             return mData.getDouble(key.mPowerColumnIndex);
-        } else {
-            return 0;
         }
+
+        double total = 0;
+        for (BatteryConsumer.Key k : mData.layout.keys) {
+            if (k.matches(powerComponent, processState, screenState, powerState)) {
+                total += mData.getDouble(k.mPowerColumnIndex);
+            }
+        }
+        return total;
     }
 
     /**
      * Total usage duration by this consumer, aggregated over the specified dimensions, in ms.
      */
     public long getUsageDurationMillis(@NonNull BatteryConsumer.Dimensions dimensions) {
-        return getUsageDurationMillis(dimensions.powerComponent, dimensions.processState,
+        return getUsageDurationMillis(dimensions.powerComponentId, dimensions.processState,
                 dimensions.screenState, dimensions.powerState);
     }
 
     /**
      * Total usage duration by this consumer, aggregated over the specified dimensions, in ms.
      */
-    public long getUsageDurationMillis(@BatteryConsumer.PowerComponent int powerComponent,
+    public long getUsageDurationMillis(@BatteryConsumer.PowerComponentId int powerComponent,
             @BatteryConsumer.ProcessState int processState,
             @BatteryConsumer.ScreenState int screenState,
             @BatteryConsumer.PowerState int powerState) {
-        if ((mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY)
-                || (mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY)) {
-            BatteryConsumer.Key key = mData.layout.getKey(powerComponent,
-                    processState, screenState, powerState);
-            if (key != null) {
-                return mData.getLong(key.mDurationColumnIndex);
-            }
-            return 0;
-        }
-
-        if (mData.layout.screenStateDataIncluded || mData.layout.powerStateDataIncluded) {
-            long total = 0;
-            for (BatteryConsumer.Key key : mData.layout.keys) {
-                if (key.processState != PROCESS_STATE_UNSPECIFIED
-                        && key.matches(powerComponent, processState, screenState, powerState)) {
-                    total += mData.getLong(key.mDurationColumnIndex);
-                }
-            }
-            if (total != 0) {
-                return total;
-            }
-        }
-
-        BatteryConsumer.Key key = mData.layout.getKey(powerComponent, processState,
-                SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED);
-        if (key != null) {
+        BatteryConsumer.Key key = mData.layout.getKey(powerComponent,
+                mData.layout.processStateDataIncluded && processState != PROCESS_STATE_ANY
+                        ? processState : PROCESS_STATE_UNSPECIFIED,
+                mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY
+                        ? screenState : SCREEN_STATE_UNSPECIFIED,
+                mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY
+                        ? powerState : POWER_STATE_UNSPECIFIED);
+        if (key != null && mData.hasValue(key.mDurationColumnIndex)) {
             return mData.getLong(key.mDurationColumnIndex);
-        } else {
-            return 0;
         }
+
+        long total = 0;
+        for (BatteryConsumer.Key k : mData.layout.keys) {
+            if (k.matches(powerComponent, processState, screenState, powerState)) {
+                total += mData.getLong(k.mDurationColumnIndex);
+            }
+        }
+        return total;
     }
 
     /**
@@ -168,39 +147,12 @@
         if (mData.hasValue(key.mPowerColumnIndex)) {
             return mData.getDouble(key.mPowerColumnIndex);
         }
-        return getConsumedPower(key.powerComponent, key.processState, key.screenState,
+        return getConsumedPower(key.powerComponentId, key.processState, key.screenState,
                 key.powerState);
     }
 
-    /**
-     * Returns the amount of drain attributed to the specified custom drain type.
-     *
-     * @param componentId The ID of the custom power component.
-     * @return Amount of consumed power in mAh.
-     */
-    public double getConsumedPowerForCustomComponent(int componentId) {
-        final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-        if (index >= 0 && index < mData.layout.customPowerComponentCount) {
-            return mData.getDouble(mData.layout.firstCustomConsumedPowerColumn + index);
-        } else {
-            throw new IllegalArgumentException(
-                    "Unsupported custom power component ID: " + componentId);
-        }
-    }
-
     public String getCustomPowerComponentName(int componentId) {
-        final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-        if (index >= 0 && index < mData.layout.customPowerComponentCount) {
-            try {
-                return mData.layout.customPowerComponentNames[index];
-            } catch (ArrayIndexOutOfBoundsException e) {
-                throw new IllegalArgumentException(
-                        "Unsupported custom power component ID: " + componentId);
-            }
-        } else {
-            throw new IllegalArgumentException(
-                    "Unsupported custom power component ID: " + componentId);
-        }
+        return mData.layout.getPowerComponentName(componentId);
     }
 
     @BatteryConsumer.PowerModel
@@ -224,63 +176,26 @@
             return mData.getLong(key.mDurationColumnIndex);
         }
 
-        return getUsageDurationMillis(key.powerComponent, key.processState, key.screenState,
+        return getUsageDurationMillis(key.powerComponentId, key.processState, key.screenState,
                 key.powerState);
     }
 
-    /**
-     * Returns the amount of usage time attributed to the specified custom component.
-     *
-     * @param componentId The ID of the custom power component.
-     * @return Amount of time in milliseconds.
-     */
-    public long getUsageDurationForCustomComponentMillis(int componentId) {
-        final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-        if (index >= 0 && index < mData.layout.customPowerComponentCount) {
-            return mData.getLong(mData.layout.firstCustomUsageDurationColumn + index);
-        } else {
-            throw new IllegalArgumentException(
-                    "Unsupported custom power component ID: " + componentId);
-        }
-    }
-
     void dump(PrintWriter pw, @BatteryConsumer.ScreenState int screenState,
             @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) {
         StringBuilder sb = new StringBuilder();
-        for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
-                componentId++) {
-            dump(sb, componentId, PROCESS_STATE_ANY, screenState, powerState, skipEmptyComponents);
+        for (@BatteryConsumer.PowerComponentId int id : mData.layout.powerComponentIds) {
+            dump(sb, id, PROCESS_STATE_ANY, screenState, powerState, skipEmptyComponents);
             if (mData.layout.processStateDataIncluded) {
                 for (int processState = 0; processState < BatteryConsumer.PROCESS_STATE_COUNT;
                         processState++) {
                     if (processState == PROCESS_STATE_UNSPECIFIED) {
                         continue;
                     }
-                    dump(sb, componentId, processState, screenState, powerState,
-                            skipEmptyComponents);
+                    dump(sb, id, processState, screenState, powerState, skipEmptyComponents);
                 }
             }
         }
 
-        // TODO(b/352835319): take into account screen and power states
-        if (screenState == SCREEN_STATE_ANY && powerState == POWER_STATE_ANY) {
-            final int customComponentCount = mData.layout.customPowerComponentCount;
-            for (int customComponentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-                    customComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                            + customComponentCount;
-                    customComponentId++) {
-                final double customComponentPower =
-                        getConsumedPowerForCustomComponent(customComponentId);
-                if (skipEmptyComponents && customComponentPower == 0) {
-                    continue;
-                }
-                sb.append(getCustomPowerComponentName(customComponentId));
-                sb.append("=");
-                sb.append(BatteryStats.formatCharge(customComponentPower));
-                sb.append(" ");
-            }
-        }
-
         // Remove trailing spaces
         while (!sb.isEmpty() && Character.isWhitespace(sb.charAt(sb.length() - 1))) {
             sb.setLength(sb.length() - 1);
@@ -289,25 +204,25 @@
         pw.println(sb);
     }
 
-    private void dump(StringBuilder sb, @BatteryConsumer.PowerComponent int powerComponent,
+    private void dump(StringBuilder sb, @BatteryConsumer.PowerComponentId int powerComponent,
             @BatteryConsumer.ProcessState int processState,
             @BatteryConsumer.ScreenState int screenState,
             @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) {
-        final double componentPower = getConsumedPower(powerComponent, processState, screenState,
+        final double power = getConsumedPower(powerComponent, processState, screenState,
                 powerState);
         final long durationMs = getUsageDurationMillis(powerComponent, processState, screenState,
                 powerState);
-        if (skipEmptyComponents && componentPower == 0 && durationMs == 0) {
+        if (skipEmptyComponents && power == 0 && durationMs == 0) {
             return;
         }
 
-        sb.append(BatteryConsumer.powerComponentIdToString(powerComponent));
-        if (processState != PROCESS_STATE_UNSPECIFIED) {
+        sb.append(mData.layout.getPowerComponentName(powerComponent));
+        if (processState != PROCESS_STATE_ANY) {
             sb.append(':');
             sb.append(BatteryConsumer.processStateToString(processState));
         }
         sb.append("=");
-        sb.append(BatteryStats.formatCharge(componentPower));
+        sb.append(BatteryStats.formatCharge(power));
 
         if (durationMs != 0) {
             sb.append(" (");
@@ -334,15 +249,14 @@
     private boolean writeStatsProtoImpl(@Nullable ProtoOutputStream proto) {
         boolean interestingData = false;
 
-        for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
-                componentId++) {
+        for (@BatteryConsumer.PowerComponentId int componentId : mData.layout.powerComponentIds) {
             final BatteryConsumer.Key[] keys = mData.layout.getKeys(componentId);
             for (BatteryConsumer.Key key : keys) {
                 final long powerDeciCoulombs = convertMahToDeciCoulombs(
-                        getConsumedPower(key.powerComponent, key.processState, key.screenState,
+                        getConsumedPower(key.powerComponentId, key.processState, key.screenState,
                                 key.powerState));
-                final long durationMs = getUsageDurationMillis(key.powerComponent, key.processState,
-                        key.screenState, key.powerState);
+                final long durationMs = getUsageDurationMillis(key.powerComponentId,
+                        key.processState, key.screenState, key.powerState);
 
                 if (powerDeciCoulombs == 0 && durationMs == 0) {
                     // No interesting data. Make sure not to even write the COMPONENT int.
@@ -356,7 +270,7 @@
                     return true;
                 }
 
-                if (key.processState == PROCESS_STATE_ANY) {
+                if (key.processState == PROCESS_STATE_UNSPECIFIED) {
                     writePowerComponentUsage(proto,
                             BatteryUsageStatsAtomsProto.BatteryConsumerData.POWER_COMPONENTS,
                             componentId, powerDeciCoulombs, durationMs);
@@ -366,27 +280,6 @@
                 }
             }
         }
-        for (int idx = 0; idx < mData.layout.customPowerComponentCount; idx++) {
-            final int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + idx;
-            final long powerDeciCoulombs =
-                    convertMahToDeciCoulombs(getConsumedPowerForCustomComponent(componentId));
-            final long durationMs = getUsageDurationForCustomComponentMillis(componentId);
-
-            if (powerDeciCoulombs == 0 && durationMs == 0) {
-                // No interesting data. Make sure not to even write the COMPONENT int.
-                continue;
-            }
-
-            interestingData = true;
-            if (proto == null) {
-                // We're just asked whether there is data, not to actually write it. And there is.
-                return true;
-            }
-
-            writePowerComponentUsage(proto,
-                    BatteryUsageStatsAtomsProto.BatteryConsumerData.POWER_COMPONENTS,
-                    componentId, powerDeciCoulombs, durationMs);
-        }
         return interestingData;
     }
 
@@ -427,8 +320,9 @@
         proto.end(slicesToken);
     }
 
-    private void writePowerComponentUsage(ProtoOutputStream proto, long tag, int componentId,
-            long powerDeciCoulombs, long durationMs) {
+    private void writePowerComponentUsage(ProtoOutputStream proto, long tag,
+            @BatteryConsumer.PowerComponentId int componentId, long powerDeciCoulombs,
+            long durationMs) {
         final long token = proto.start(tag);
         proto.write(
                 BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage
@@ -460,7 +354,7 @@
             }
 
             serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
-            serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, key.powerComponent);
+            serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, key.powerComponentId);
             if (key.processState != PROCESS_STATE_UNSPECIFIED) {
                 serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE,
                         key.processState);
@@ -485,32 +379,11 @@
             }
             serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
         }
-
-        final int customComponentEnd = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                + mData.layout.customPowerComponentCount;
-        for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-                componentId < customComponentEnd;
-                componentId++) {
-            final double powerMah = getConsumedPowerForCustomComponent(componentId);
-            final long durationMs = getUsageDurationForCustomComponentMillis(componentId);
-            if (powerMah == 0 && durationMs == 0) {
-                continue;
-            }
-
-            serializer.startTag(null, BatteryUsageStats.XML_TAG_CUSTOM_COMPONENT);
-            serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, componentId);
-            if (powerMah != 0) {
-                serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah);
-            }
-            if (durationMs != 0) {
-                serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs);
-            }
-            serializer.endTag(null, BatteryUsageStats.XML_TAG_CUSTOM_COMPONENT);
-        }
-
         serializer.endTag(null, BatteryUsageStats.XML_TAG_POWER_COMPONENTS);
     }
 
+    // No longer part of the BatteryUsageStats XML format. Preserved for compatibility
+    private static final String XML_TAG_CUSTOM_COMPONENT_COMPAT = "custom_component";
 
     static void parseXml(TypedXmlPullParser parser, PowerComponents.Builder builder)
             throws XmlPullParserException, IOException {
@@ -525,7 +398,8 @@
                 && eventType != XmlPullParser.END_DOCUMENT) {
             if (eventType == XmlPullParser.START_TAG) {
                 switch (parser.getName()) {
-                    case BatteryUsageStats.XML_TAG_COMPONENT: {
+                    case BatteryUsageStats.XML_TAG_COMPONENT:
+                    case XML_TAG_CUSTOM_COMPONENT_COMPAT: {
                         int componentId = -1;
                         int processState = PROCESS_STATE_UNSPECIFIED;
                         int screenState = SCREEN_STATE_UNSPECIFIED;
@@ -564,27 +438,6 @@
                         builder.setUsageDurationMillis(key, durationMs);
                         break;
                     }
-                    case BatteryUsageStats.XML_TAG_CUSTOM_COMPONENT: {
-                        int componentId = -1;
-                        double powerMah = 0;
-                        long durationMs = 0;
-                        for (int i = 0; i < parser.getAttributeCount(); i++) {
-                            switch (parser.getAttributeName(i)) {
-                                case BatteryUsageStats.XML_ATTR_ID:
-                                    componentId = parser.getAttributeInt(i);
-                                    break;
-                                case BatteryUsageStats.XML_ATTR_POWER:
-                                    powerMah = parser.getAttributeDouble(i);
-                                    break;
-                                case BatteryUsageStats.XML_ATTR_DURATION:
-                                    durationMs = parser.getAttributeLong(i);
-                                    break;
-                            }
-                        }
-                        builder.setConsumedPowerForCustomComponent(componentId, powerMah);
-                        builder.setUsageDurationForCustomComponentMillis(componentId, durationMs);
-                        break;
-                    }
                 }
             }
             eventType = parser.next();
@@ -631,36 +484,6 @@
             return this;
         }
 
-        /**
-         * Sets the amount of drain attributed to the specified custom drain type.
-         *
-         * @param componentId    The ID of the custom power component.
-         * @param componentPower Amount of consumed power in mAh.
-         */
-        @NonNull
-        public Builder setConsumedPowerForCustomComponent(int componentId, double componentPower) {
-            final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-            if (index < 0 || index >= mData.layout.customPowerComponentCount) {
-                throw new IllegalArgumentException(
-                        "Unsupported custom power component ID: " + componentId);
-            }
-            mData.putDouble(mData.layout.firstCustomConsumedPowerColumn + index, componentPower);
-            return this;
-        }
-
-        @NonNull
-        public Builder addConsumedPowerForCustomComponent(int componentId, double componentPower) {
-            final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-            if (index < 0 || index >= mData.layout.customPowerComponentCount) {
-                throw new IllegalArgumentException(
-                        "Unsupported custom power component ID: " + componentId);
-            }
-            mData.putDouble(mData.layout.firstCustomConsumedPowerColumn + index,
-                    mData.getDouble(mData.layout.firstCustomConsumedPowerColumn + index)
-                            + componentPower);
-            return this;
-        }
-
         @NonNull
         public Builder setUsageDurationMillis(BatteryConsumer.Key key,
                 long componentUsageDurationMillis) {
@@ -668,26 +491,6 @@
             return this;
         }
 
-        /**
-         * Sets the amount of time used by the specified custom component.
-         *
-         * @param componentId                  The ID of the custom power component.
-         * @param componentUsageDurationMillis Amount of time in milliseconds.
-         */
-        @NonNull
-        public Builder setUsageDurationForCustomComponentMillis(int componentId,
-                long componentUsageDurationMillis) {
-            final int index = componentId - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-            if (index < 0 || index >= mData.layout.customPowerComponentCount) {
-                throw new IllegalArgumentException(
-                        "Unsupported custom power component ID: " + componentId);
-            }
-
-            mData.putLong(mData.layout.firstCustomUsageDurationColumn + index,
-                    componentUsageDurationMillis);
-            return this;
-        }
-
         public void addPowerAndDuration(PowerComponents.Builder other) {
             addPowerAndDuration(other.mData);
         }
@@ -706,19 +509,23 @@
             }
 
             for (BatteryConsumer.Key key : mData.layout.keys) {
-                BatteryConsumer.Key otherKey = otherData.layout.getKey(key.powerComponent,
+                BatteryConsumer.Key otherKey = otherData.layout.getKey(key.powerComponentId,
                         key.processState, key.screenState, key.powerState);
                 if (otherKey == null) {
                     continue;
                 }
-
-                mData.putDouble(key.mPowerColumnIndex,
-                        mData.getDouble(key.mPowerColumnIndex)
-                                + otherData.getDouble(otherKey.mPowerColumnIndex));
-                mData.putLong(key.mDurationColumnIndex,
-                        mData.getLong(key.mDurationColumnIndex)
-                                + otherData.getLong(otherKey.mDurationColumnIndex));
-
+                if (mData.hasValue(key.mPowerColumnIndex)
+                        || otherData.hasValue(otherKey.mPowerColumnIndex)) {
+                    mData.putDouble(key.mPowerColumnIndex,
+                            mData.getDouble(key.mPowerColumnIndex)
+                                    + otherData.getDouble(otherKey.mPowerColumnIndex));
+                }
+                if (mData.hasValue(key.mDurationColumnIndex)
+                        || otherData.hasValue(otherKey.mDurationColumnIndex)) {
+                    mData.putLong(key.mDurationColumnIndex,
+                            mData.getLong(key.mDurationColumnIndex)
+                                    + otherData.getLong(otherKey.mDurationColumnIndex));
+                }
                 if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) {
                     continue;
                 }
@@ -742,21 +549,6 @@
                             BatteryConsumer.POWER_MODEL_UNDEFINED);
                 }
             }
-
-            for (int i = mData.layout.customPowerComponentCount - 1; i >= 0; i--) {
-                final int powerColumnIndex = mData.layout.firstCustomConsumedPowerColumn + i;
-                final int otherPowerColumnIndex =
-                        otherData.layout.firstCustomConsumedPowerColumn + i;
-                mData.putDouble(powerColumnIndex,
-                        mData.getDouble(powerColumnIndex) + otherData.getDouble(
-                                otherPowerColumnIndex));
-
-                final int usageColumnIndex = mData.layout.firstCustomUsageDurationColumn + i;
-                final int otherDurationColumnIndex =
-                        otherData.layout.firstCustomUsageDurationColumn + i;
-                mData.putLong(usageColumnIndex, mData.getLong(usageColumnIndex)
-                        + otherData.getLong(otherDurationColumnIndex));
-            }
         }
 
         /**
@@ -765,15 +557,12 @@
          */
         public double getTotalPower() {
             double totalPowerMah = 0;
-            for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
-                    componentId++) {
-                totalPowerMah += mData.getDouble(
-                        mData.layout.getKeyOrThrow(componentId, PROCESS_STATE_ANY, SCREEN_STATE_ANY,
-                                POWER_STATE_ANY).mPowerColumnIndex);
-            }
-            for (int i = 0; i < mData.layout.customPowerComponentCount; i++) {
-                totalPowerMah += mData.getDouble(
-                        mData.layout.firstCustomConsumedPowerColumn + i);
+            for (BatteryConsumer.Key key : mData.layout.keys) {
+                if (key.processState == PROCESS_STATE_UNSPECIFIED
+                        && key.screenState == SCREEN_STATE_UNSPECIFIED
+                        && key.powerState == POWER_STATE_UNSPECIFIED) {
+                    totalPowerMah += mData.getDouble(key.mPowerColumnIndex);
+                }
             }
             return totalPowerMah;
         }
@@ -783,7 +572,7 @@
          */
         @NonNull
         public PowerComponents build() {
-            for (BatteryConsumer.Key key: mData.layout.keys) {
+            for (BatteryConsumer.Key key : mData.layout.keys) {
                 if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) {
                     if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) {
                         mData.putInt(key.mPowerModelColumnIndex,
@@ -798,9 +587,7 @@
                 }
             }
 
-            if (mData.getDouble(mData.layout.totalConsumedPowerColumnIndex) == 0) {
-                mData.putDouble(mData.layout.totalConsumedPowerColumnIndex, getTotalPower());
-            }
+            mData.putDouble(mData.layout.totalConsumedPowerColumnIndex, getTotalPower());
             return new PowerComponents(this);
         }
     }
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 013ec5f..17d2790 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -28,9 +28,7 @@
 import android.util.Log;
 import android.view.WindowManager;
 
-import java.lang.ref.WeakReference;
 import java.util.concurrent.Executor;
-import java.util.function.Consumer;
 
 
 /**
@@ -54,51 +52,43 @@
     // An {@link IDreamOverlayClient} implementation that identifies itself when forwarding
     // requests to the {@link DreamOverlayService}
     private static class OverlayClient extends IDreamOverlayClient.Stub {
-        private final WeakReference<DreamOverlayService> mService;
+        private final DreamOverlayService mService;
         private boolean mShowComplications;
         private ComponentName mDreamComponent;
         IDreamOverlayCallback mDreamOverlayCallback;
 
-        OverlayClient(WeakReference<DreamOverlayService> service) {
+        OverlayClient(DreamOverlayService service) {
             mService = service;
         }
 
-        private void applyToDream(Consumer<DreamOverlayService> consumer) {
-            final DreamOverlayService service = mService.get();
-
-            if (service != null) {
-                consumer.accept(service);
-            }
-        }
-
         @Override
         public void startDream(WindowManager.LayoutParams params, IDreamOverlayCallback callback,
                 String dreamComponent, boolean shouldShowComplications) throws RemoteException {
             mDreamComponent = ComponentName.unflattenFromString(dreamComponent);
             mShowComplications = shouldShowComplications;
             mDreamOverlayCallback = callback;
-            applyToDream(dreamOverlayService -> dreamOverlayService.startDream(this, params));
+            mService.startDream(this, params);
         }
 
         @Override
         public void wakeUp() {
-            applyToDream(dreamOverlayService -> dreamOverlayService.wakeUp(this));
+            mService.wakeUp(this);
         }
 
         @Override
         public void endDream() {
-            applyToDream(dreamOverlayService -> dreamOverlayService.endDream(this));
+            mService.endDream(this);
         }
 
         @Override
         public void comeToFront() {
-            applyToDream(dreamOverlayService -> dreamOverlayService.comeToFront(this));
+            mService.comeToFront(this);
         }
 
         @Override
         public void onWakeRequested() {
             if (Flags.dreamWakeRedirect()) {
-                applyToDream(DreamOverlayService::onWakeRequested);
+                mService.onWakeRequested();
             }
         }
 
@@ -171,24 +161,17 @@
         });
     }
 
-    private static class DreamOverlay extends IDreamOverlay.Stub {
-        private final WeakReference<DreamOverlayService> mService;
-
-        DreamOverlay(DreamOverlayService service) {
-            mService = new WeakReference<>(service);
-        }
-
+    private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
         @Override
         public void getClient(IDreamOverlayClientCallback callback) {
             try {
-                callback.onDreamOverlayClient(new OverlayClient(mService));
+                callback.onDreamOverlayClient(
+                        new OverlayClient(DreamOverlayService.this));
             } catch (RemoteException e) {
                 Log.e(TAG, "could not send client to callback", e);
             }
         }
-    }
-
-    private final IDreamOverlay mDreamOverlay = new DreamOverlay(this);
+    };
 
     public DreamOverlayService() {
     }
@@ -212,12 +195,6 @@
         }
     }
 
-    @Override
-    public void onDestroy() {
-        mCurrentClient = null;
-        super.onDestroy();
-    }
-
     @Nullable
     @Override
     public final IBinder onBind(@NonNull Intent intent) {
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index fc6c2e8..57acc71 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -187,6 +187,13 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface ConfigOrigin {}
 
+    /**
+     * Prefix for the ids of implicit Zen rules. Implicit rules are those created automatically
+     * on behalf of apps that call {@link NotificationManager#setNotificationPolicy} or
+     * {@link NotificationManager#setInterruptionFilter}.
+     */
+    private static final String IMPLICIT_RULE_ID_PREFIX = "implicit_"; // + pkg_name
+
     public static final int SOURCE_ANYONE = Policy.PRIORITY_SENDERS_ANY;
     public static final int SOURCE_CONTACT = Policy.PRIORITY_SENDERS_CONTACTS;
     public static final int SOURCE_STAR = Policy.PRIORITY_SENDERS_STARRED;
@@ -2492,6 +2499,16 @@
 
     // ==== End built-in system conditions ====
 
+    /** Generate the rule id for the implicit rule for the specified package. */
+    public static String implicitRuleId(String forPackage) {
+        return IMPLICIT_RULE_ID_PREFIX + forPackage;
+    }
+
+    /** Returns whether the rule id corresponds to an implicit rule. */
+    public static boolean isImplicitRuleId(@NonNull String ruleId) {
+        return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX);
+    }
+
     private static int[] tryParseHourAndMinute(String value) {
         if (TextUtils.isEmpty(value)) return null;
         final int i = value.indexOf('.');
diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java
index d72207d..ee5bd65 100644
--- a/core/java/com/android/internal/app/IntentForwarderActivity.java
+++ b/core/java/com/android/internal/app/IntentForwarderActivity.java
@@ -48,6 +48,8 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.graphics.Insets;
 import android.graphics.drawable.Drawable;
 import android.metrics.LogMaker;
 import android.os.Build;
@@ -60,6 +62,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.view.View;
+import android.view.WindowInsets;
 import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -117,6 +120,12 @@
     }
 
     @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        setMiniresolverPadding();
+    }
+
+    @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mInjector = createInjector();
@@ -333,8 +342,7 @@
         icon.setImageDrawable(
                 getAppIcon(target, launchIntent, targetUserId, pmForTargetUser));
 
-        View buttonContainer = findViewById(R.id.button_bar_container);
-        buttonContainer.setPadding(0, 0, 0, buttonContainer.getPaddingBottom());
+        setMiniresolverPadding();
 
         ((TextView) findViewById(R.id.open_cross_profile)).setText(
                 resolverTitle);
@@ -675,6 +683,17 @@
                 && android.multiuser.Flags.enablePrivateSpaceIntentRedirection();
     }
 
+    private void setMiniresolverPadding() {
+        Insets systemWindowInsets =
+                getWindowManager().getCurrentWindowMetrics().getWindowInsets().getInsets(
+                        WindowInsets.Type.systemBars());
+
+        View buttonContainer = findViewById(R.id.button_bar_container);
+        buttonContainer.setPadding(0, 0, 0,
+                systemWindowInsets.bottom + getResources().getDimensionPixelOffset(
+                        R.dimen.resolver_button_bar_spacing));
+    }
+
     @VisibleForTesting
     protected Injector createInjector() {
         return new InjectorImpl();
diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java
index 488e06f..aafef6c 100644
--- a/core/java/com/android/internal/os/PowerStats.java
+++ b/core/java/com/android/internal/os/PowerStats.java
@@ -100,6 +100,7 @@
          * to; or a custom power component ID (if the value
          * is &gt;= {@link BatteryConsumer#FIRST_CUSTOM_POWER_COMPONENT_ID}).
          */
+        @BatteryConsumer.PowerComponentId
         public final int powerComponentId;
         public final String name;
 
@@ -142,9 +143,10 @@
                     extras);
         }
 
-        public Descriptor(int customPowerComponentId, String name, int statsArrayLength,
-                @Nullable SparseArray<String> stateLabels, int stateStatsArrayLength,
-                int uidStatsArrayLength, @NonNull PersistableBundle extras) {
+        public Descriptor(@BatteryConsumer.PowerComponentId int powerComponentId, String name,
+                int statsArrayLength, @Nullable SparseArray<String> stateLabels,
+                int stateStatsArrayLength, int uidStatsArrayLength,
+                @NonNull PersistableBundle extras) {
             if (statsArrayLength > MAX_STATS_ARRAY_LENGTH) {
                 throw new IllegalArgumentException(
                         "statsArrayLength is too high. Max = " + MAX_STATS_ARRAY_LENGTH);
@@ -157,7 +159,7 @@
                 throw new IllegalArgumentException(
                         "uidStatsArrayLength is too high. Max = " + MAX_UID_STATS_ARRAY_LENGTH);
             }
-            this.powerComponentId = customPowerComponentId;
+            this.powerComponentId = powerComponentId;
             this.name = name;
             this.statsArrayLength = statsArrayLength;
             this.stateLabels = stateLabels != null ? stateLabels : new SparseArray<>();
diff --git a/core/res/res/values-watch/themes_material.xml b/core/res/res/values-watch/themes_material.xml
index 674b3bc..001a0fc 100644
--- a/core/res/res/values-watch/themes_material.xml
+++ b/core/res/res/values-watch/themes_material.xml
@@ -43,11 +43,13 @@
     <!-- Override behaviour to set the theme colours for dialogs, keep them the same. -->
     <style name="ThemeOverlay.Material.Dialog" parent="ThemeOverlay.Material.BaseDialog">
         <item name="android:windowFullscreen">true</item>
+        <item name="backgroundDimEnabled">false</item>
     </style>
 
     <!-- Force the background and floating colours to be the default colours. -->
     <style name="Theme.Material.Dialog" parent="Theme.Material.BaseDialog">
         <item name="android:windowFullscreen">true</item>
+        <item name="backgroundDimEnabled">false</item>
         <item name="colorBackground">@color/background_material_dark</item>
         <item name="colorBackgroundFloating">@color/background_floating_material_dark</item>
         <item name="colorBackgroundCacheHint">@color/background_cache_hint_selector_material_dark</item>
@@ -56,6 +58,7 @@
     <!-- Force the background and floating colours to be the default colours. -->
     <style name="Theme.Material.Dialog.Alert" parent="Theme.Material.Dialog.BaseAlert">
         <item name="android:windowFullscreen">true</item>
+        <item name="backgroundDimEnabled">false</item>
         <item name="colorBackground">@color/background_material_dark</item>
         <item name="colorBackgroundFloating">@color/background_floating_material_dark</item>
         <item name="colorBackgroundCacheHint">@color/background_cache_hint_selector_material_dark</item>
@@ -64,6 +67,7 @@
     <!-- Force the background and floating colours to be the default colours. -->
     <style name="Theme.Material.Light.Dialog" parent="Theme.Material.Light.BaseDialog">
         <item name="android:windowFullscreen">true</item>
+        <item name="backgroundDimEnabled">false</item>
         <item name="colorBackground">@color/background_material_light</item>
         <item name="colorBackgroundFloating">@color/background_floating_material_light</item>
         <item name="colorBackgroundCacheHint">@color/background_cache_hint_selector_material_light</item>
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
index e02c89a..c729442 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png
index e02c89a..c729442 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt
new file mode 100644
index 0000000..9fdde128
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.content.res.Resources
+import android.graphics.Color
+import android.os.Handler
+import android.os.UserManager
+import android.view.IWindowManager
+import android.view.WindowManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.protolog.ProtoLog
+import com.android.internal.statusbar.IStatusBarService
+import com.android.launcher3.icons.BubbleIconFactory
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.WindowManagerShellWrapper
+import com.android.wm.shell.bubbles.properties.BubbleProperties
+import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayInsetsController
+import com.android.wm.shell.common.FloatingContentCoordinator
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.common.TaskStackListenerImpl
+import com.android.wm.shell.shared.TransactionPool
+import com.android.wm.shell.sysui.ShellCommandHandler
+import com.android.wm.shell.sysui.ShellController
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.taskview.TaskView
+import com.android.wm.shell.taskview.TaskViewTransitions
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/** Test inflating bubbles with [BubbleViewInfoTask]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleViewInfoTaskTest {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var metadataFlagListener: Bubbles.BubbleMetadataFlagListener
+    private lateinit var iconFactory: BubbleIconFactory
+    private lateinit var bubbleController: BubbleController
+    private lateinit var mainExecutor: TestExecutor
+    private lateinit var bgExecutor: TestExecutor
+    private lateinit var bubbleStackView: BubbleStackView
+    private lateinit var bubblePositioner: BubblePositioner
+    private lateinit var expandedViewManager: BubbleExpandedViewManager
+
+    private val bubbleTaskViewFactory = BubbleTaskViewFactory {
+        BubbleTaskView(mock<TaskView>(), directExecutor())
+    }
+
+    @Before
+    fun setUp() {
+        ProtoLog.REQUIRE_PROTOLOGTOOL = false
+        metadataFlagListener = Bubbles.BubbleMetadataFlagListener {}
+        iconFactory =
+            BubbleIconFactory(
+                context,
+                60,
+                30,
+                Color.RED,
+                context.resources.getDimensionPixelSize(R.dimen.importance_ring_stroke_width)
+            )
+
+        mainExecutor = TestExecutor()
+        bgExecutor = TestExecutor()
+        val windowManager = context.getSystemService(WindowManager::class.java)
+        val shellInit = ShellInit(mainExecutor)
+        val shellCommandHandler = ShellCommandHandler()
+        val shellController =
+            ShellController(
+                context,
+                shellInit,
+                shellCommandHandler,
+                mock<DisplayInsetsController>(),
+                mainExecutor
+            )
+        bubblePositioner = BubblePositioner(context, windowManager)
+        val bubbleData =
+            BubbleData(
+                context,
+                mock<BubbleLogger>(),
+                bubblePositioner,
+                BubbleEducationController(context),
+                mainExecutor,
+                bgExecutor
+            )
+
+        val surfaceSynchronizer = { obj: Runnable -> obj.run() }
+
+        val bubbleDataRepository =
+            BubbleDataRepository(
+                mock<LauncherApps>(),
+                mainExecutor,
+                bgExecutor,
+                BubblePersistentRepository(context)
+            )
+
+        bubbleController =
+            BubbleController(
+                context,
+                shellInit,
+                shellCommandHandler,
+                shellController,
+                bubbleData,
+                surfaceSynchronizer,
+                FloatingContentCoordinator(),
+                bubbleDataRepository,
+                mock<IStatusBarService>(),
+                windowManager,
+                WindowManagerShellWrapper(mainExecutor),
+                mock<UserManager>(),
+                mock<LauncherApps>(),
+                mock<BubbleLogger>(),
+                mock<TaskStackListenerImpl>(),
+                mock<ShellTaskOrganizer>(),
+                bubblePositioner,
+                mock<DisplayController>(),
+                null,
+                null,
+                mainExecutor,
+                mock<Handler>(),
+                bgExecutor,
+                mock<TaskViewTransitions>(),
+                mock<Transitions>(),
+                SyncTransactionQueue(TransactionPool(), mainExecutor),
+                mock<IWindowManager>(),
+                mock<BubbleProperties>()
+            )
+
+        val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController)
+        bubbleStackView =
+            BubbleStackView(
+                context,
+                bubbleStackViewManager,
+                bubblePositioner,
+                bubbleData,
+                surfaceSynchronizer,
+                FloatingContentCoordinator(),
+                bubbleController,
+                mainExecutor
+            )
+        expandedViewManager = BubbleExpandedViewManager.fromBubbleController(bubbleController)
+    }
+
+    @Test
+    fun start_runsOnExecutors() {
+        val bubble = createBubbleWithShortcut()
+        val task = createBubbleViewInfoTask(bubble)
+
+        task.start()
+
+        assertThat(bubble.isInflated).isFalse()
+        assertThat(bubble.expandedView).isNull()
+        assertThat(task.isFinished).isFalse()
+
+        bgExecutor.flushAll()
+        assertThat(bubble.isInflated).isFalse()
+        assertThat(bubble.expandedView).isNull()
+        assertThat(task.isFinished).isFalse()
+
+        mainExecutor.flushAll()
+        assertThat(bubble.isInflated).isTrue()
+        assertThat(bubble.expandedView).isNotNull()
+        assertThat(task.isFinished).isTrue()
+    }
+
+    @Test
+    fun startSync_runsImmediately() {
+        val bubble = createBubbleWithShortcut()
+        val task = createBubbleViewInfoTask(bubble)
+
+        task.startSync()
+        assertThat(bubble.isInflated).isTrue()
+        assertThat(bubble.expandedView).isNotNull()
+        assertThat(task.isFinished).isTrue()
+    }
+
+    @Test
+    fun start_calledTwice_throwsIllegalStateException() {
+        val bubble = createBubbleWithShortcut()
+        val task = createBubbleViewInfoTask(bubble)
+        task.start()
+        Assert.assertThrows(IllegalStateException::class.java) { task.start() }
+    }
+
+    @Test
+    fun startSync_calledTwice_throwsIllegalStateException() {
+        val bubble = createBubbleWithShortcut()
+        val task = createBubbleViewInfoTask(bubble)
+        task.startSync()
+        Assert.assertThrows(IllegalStateException::class.java) { task.startSync() }
+    }
+
+    @Test
+    fun start_callbackNotified() {
+        val bubble = createBubbleWithShortcut()
+        var bubbleFromCallback: Bubble? = null
+        val callback = BubbleViewInfoTask.Callback { b: Bubble? -> bubbleFromCallback = b }
+        val task = createBubbleViewInfoTask(bubble, callback)
+        task.start()
+        bgExecutor.flushAll()
+        mainExecutor.flushAll()
+        assertThat(bubbleFromCallback).isSameInstanceAs(bubble)
+    }
+
+    @Test
+    fun startSync_callbackNotified() {
+        val bubble = createBubbleWithShortcut()
+        var bubbleFromCallback: Bubble? = null
+        val callback = BubbleViewInfoTask.Callback { b: Bubble? -> bubbleFromCallback = b }
+        val task = createBubbleViewInfoTask(bubble, callback)
+        task.startSync()
+        assertThat(bubbleFromCallback).isSameInstanceAs(bubble)
+    }
+
+    @Test
+    fun cancel_beforeBackgroundWorkStarts_bubbleNotInflated() {
+        val bubble = createBubbleWithShortcut()
+        val task = createBubbleViewInfoTask(bubble)
+        task.start()
+
+        // Cancel before allowing background or main executor to run
+        task.cancel()
+        bgExecutor.flushAll()
+        mainExecutor.flushAll()
+
+        assertThat(bubble.isInflated).isFalse()
+        assertThat(bubble.expandedView).isNull()
+        assertThat(task.isFinished).isTrue()
+    }
+
+    @Test
+    fun cancel_afterBackgroundWorkBeforeMainThreadWork_bubbleNotInflated() {
+        val bubble = createBubbleWithShortcut()
+        val task = createBubbleViewInfoTask(bubble)
+        task.start()
+
+        // Cancel after background executor runs, but before main executor runs
+        bgExecutor.flushAll()
+        task.cancel()
+        mainExecutor.flushAll()
+
+        assertThat(bubble.isInflated).isFalse()
+        assertThat(bubble.expandedView).isNull()
+        assertThat(task.isFinished).isTrue()
+    }
+
+    @Test
+    fun cancel_beforeStart_bubbleNotInflated() {
+        val bubble = createBubbleWithShortcut()
+        val task = createBubbleViewInfoTask(bubble)
+        task.cancel()
+        task.start()
+        bgExecutor.flushAll()
+        mainExecutor.flushAll()
+
+        assertThat(task.isFinished).isTrue()
+        assertThat(bubble.isInflated).isFalse()
+        assertThat(bubble.expandedView).isNull()
+    }
+
+    private fun createBubbleWithShortcut(): Bubble {
+        val shortcutInfo = ShortcutInfo.Builder(context, "mockShortcutId").build()
+        return Bubble(
+            "mockKey",
+            shortcutInfo,
+            1000,
+            Resources.ID_NULL,
+            "mockTitle",
+            0 /* taskId */,
+            "mockLocus",
+            true /* isDismissible */,
+            mainExecutor,
+            bgExecutor,
+            metadataFlagListener
+        )
+    }
+
+    private fun createBubbleViewInfoTask(
+        bubble: Bubble,
+        callback: BubbleViewInfoTask.Callback? = null
+    ): BubbleViewInfoTask {
+        return BubbleViewInfoTask(
+            bubble,
+            context,
+            expandedViewManager,
+            bubbleTaskViewFactory,
+            bubblePositioner,
+            bubbleStackView,
+            null /* layerView */,
+            iconFactory,
+            false /* skipInflation */,
+            callback,
+            mainExecutor,
+            bgExecutor
+        )
+    }
+
+    private class TestExecutor : ShellExecutor {
+
+        private val runnables: MutableList<Runnable> = mutableListOf()
+
+        override fun execute(runnable: Runnable) {
+            runnables.add(runnable)
+        }
+
+        override fun executeDelayed(runnable: Runnable, delayMillis: Long) {
+            execute(runnable)
+        }
+
+        override fun removeCallbacks(runnable: Runnable?) {}
+
+        override fun hasCallback(runnable: Runnable?): Boolean = false
+
+        fun flushAll() {
+            while (runnables.isNotEmpty()) {
+                runnables.removeAt(0).run()
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 021d3c3..3e758bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -568,11 +568,11 @@
             @Nullable BubbleBarLayerView layerView,
             BubbleIconFactory iconFactory,
             boolean skipInflation) {
+        ProtoLog.v(WM_SHELL_BUBBLES, "Inflate bubble key=%s", getKey());
         if (Flags.bubbleViewInfoExecutors()) {
-            if (mInflationTask != null && mInflationTask.getStatus() != FINISHED) {
-                mInflationTask.cancel(true /* mayInterruptIfRunning */);
+            if (mInflationTask != null && !mInflationTask.isFinished()) {
+                mInflationTask.cancel();
             }
-            // TODO(b/353894869): switch to executors
             mInflationTask = new BubbleViewInfoTask(this,
                     context,
                     expandedViewManager,
@@ -583,11 +583,12 @@
                     iconFactory,
                     skipInflation,
                     callback,
-                    mMainExecutor);
+                    mMainExecutor,
+                    mBgExecutor);
             if (mInflateSynchronously) {
-                mInflationTask.onPostExecute(mInflationTask.doInBackground());
+                mInflationTask.startSync();
             } else {
-                mInflationTask.execute();
+                mInflationTask.start();
             }
         } else {
             if (mInflationTaskLegacy != null && mInflationTaskLegacy.getStatus() != FINISHED) {
@@ -625,7 +626,7 @@
             if (mInflationTask == null) {
                 return;
             }
-            mInflationTask.cancel(true /* mayInterruptIfRunning */);
+            mInflationTask.cancel();
         } else {
             if (mInflationTaskLegacy == null) {
                 return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index 03a2efd..13855f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -20,6 +20,7 @@
 import static com.android.wm.shell.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,13 +35,13 @@
 import android.graphics.Path;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
-import android.os.AsyncTask;
 import android.util.Log;
 import android.util.PathParser;
 import android.view.LayoutInflater;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
+import com.android.internal.protolog.ProtoLog;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.BubbleIconFactory;
 import com.android.wm.shell.R;
@@ -50,15 +51,14 @@
 import java.lang.ref.WeakReference;
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Simple task to inflate views & load necessary info to display a bubble.
  */
-// TODO(b/353894869): switch to executors
-public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> {
+public class BubbleViewInfoTask {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES;
 
-
     /**
      * Callback to find out when the bubble has been inflated & necessary data loaded.
      */
@@ -69,17 +69,22 @@
         void onBubbleViewsReady(Bubble bubble);
     }
 
-    private Bubble mBubble;
-    private WeakReference<Context> mContext;
-    private WeakReference<BubbleExpandedViewManager> mExpandedViewManager;
-    private WeakReference<BubbleTaskViewFactory> mTaskViewFactory;
-    private WeakReference<BubblePositioner> mPositioner;
-    private WeakReference<BubbleStackView> mStackView;
-    private WeakReference<BubbleBarLayerView> mLayerView;
-    private BubbleIconFactory mIconFactory;
-    private boolean mSkipInflation;
-    private Callback mCallback;
-    private Executor mMainExecutor;
+    private final Bubble mBubble;
+    private final WeakReference<Context> mContext;
+    private final WeakReference<BubbleExpandedViewManager> mExpandedViewManager;
+    private final WeakReference<BubbleTaskViewFactory> mTaskViewFactory;
+    private final WeakReference<BubblePositioner> mPositioner;
+    private final WeakReference<BubbleStackView> mStackView;
+    private final WeakReference<BubbleBarLayerView> mLayerView;
+    private final BubbleIconFactory mIconFactory;
+    private final boolean mSkipInflation;
+    private final Callback mCallback;
+    private final Executor mMainExecutor;
+    private final Executor mBgExecutor;
+
+    private final AtomicBoolean mStarted = new AtomicBoolean();
+    private final AtomicBoolean mCancelled = new AtomicBoolean();
+    private final AtomicBoolean mFinished = new AtomicBoolean();
 
     /**
      * Creates a task to load information for the provided {@link Bubble}. Once all info
@@ -95,7 +100,8 @@
             BubbleIconFactory factory,
             boolean skipInflation,
             Callback c,
-            Executor mainExecutor) {
+            Executor mainExecutor,
+            Executor bgExecutor) {
         mBubble = b;
         mContext = new WeakReference<>(context);
         mExpandedViewManager = new WeakReference<>(expandedViewManager);
@@ -107,40 +113,123 @@
         mSkipInflation = skipInflation;
         mCallback = c;
         mMainExecutor = mainExecutor;
+        mBgExecutor = bgExecutor;
     }
 
-    @Override
-    protected BubbleViewInfo doInBackground(Void... voids) {
+    /**
+     * Load bubble view info in background using {@code bgExecutor} specified in constructor.
+     * <br>
+     * Use {@link #cancel()} to stop the task.
+     *
+     * @throws IllegalStateException if the task is already started
+     */
+    public void start() {
+        verifyCanStart();
+        if (mCancelled.get()) {
+            // We got cancelled even before start was called. Exit early
+            mFinished.set(true);
+            return;
+        }
+        mBgExecutor.execute(() -> {
+            if (mCancelled.get()) {
+                // We got cancelled while background executor was busy and this was waiting
+                mFinished.set(true);
+                return;
+            }
+            BubbleViewInfo viewInfo = loadViewInfo();
+            if (mCancelled.get()) {
+                // Do not schedule anything on main executor if we got cancelled.
+                // Loading view info involves inflating views and it is possible we get cancelled
+                // during it.
+                mFinished.set(true);
+                return;
+            }
+            mMainExecutor.execute(() -> {
+                // Before updating view info check that we did not get cancelled while waiting
+                // main executor to pick up the work
+                if (!mCancelled.get()) {
+                    updateViewInfo(viewInfo);
+                }
+                mFinished.set(true);
+            });
+        });
+    }
+
+    private void verifyCanStart() {
+        if (mStarted.getAndSet(true)) {
+            throw new IllegalStateException("Task already started");
+        }
+    }
+
+    /**
+     * Load bubble view info synchronously.
+     *
+     * @throws IllegalStateException if the task is already started
+     */
+    public void startSync() {
+        verifyCanStart();
+        if (mCancelled.get()) {
+            mFinished.set(true);
+            return;
+        }
+        updateViewInfo(loadViewInfo());
+        mFinished.set(true);
+    }
+
+    /**
+     * Cancel the task. Stops the task from running if called before {@link #start()} or
+     * {@link #startSync()}
+     */
+    public void cancel() {
+        mCancelled.set(true);
+    }
+
+    /**
+     * Return {@code true} when the task has completed loading the view info.
+     */
+    public boolean isFinished() {
+        return mFinished.get();
+    }
+
+    @Nullable
+    private BubbleViewInfo loadViewInfo() {
         if (!verifyState()) {
             // If we're in an inconsistent state, then switched modes and should just bail now.
             return null;
         }
+        ProtoLog.v(WM_SHELL_BUBBLES, "Task loading bubble view info key=%s", mBubble.getKey());
         if (mLayerView.get() != null) {
-            return BubbleViewInfo.populateForBubbleBar(mContext.get(), mExpandedViewManager.get(),
-                    mTaskViewFactory.get(), mPositioner.get(), mLayerView.get(), mIconFactory,
-                    mBubble, mSkipInflation);
+            return BubbleViewInfo.populateForBubbleBar(mContext.get(), mTaskViewFactory.get(),
+                    mLayerView.get(), mIconFactory, mBubble, mSkipInflation);
         } else {
-            return BubbleViewInfo.populate(mContext.get(), mExpandedViewManager.get(),
-                    mTaskViewFactory.get(), mPositioner.get(), mStackView.get(), mIconFactory,
-                    mBubble, mSkipInflation);
+            return BubbleViewInfo.populate(mContext.get(), mTaskViewFactory.get(),
+                    mPositioner.get(), mStackView.get(), mIconFactory, mBubble, mSkipInflation);
         }
     }
 
-    @Override
-    protected void onPostExecute(BubbleViewInfo viewInfo) {
-        if (isCancelled() || viewInfo == null) {
+    private void updateViewInfo(@Nullable BubbleViewInfo viewInfo) {
+        if (viewInfo == null || !verifyState()) {
             return;
         }
+        ProtoLog.v(WM_SHELL_BUBBLES, "Task updating bubble view info key=%s", mBubble.getKey());
+        if (!mBubble.isInflated()) {
+            if (viewInfo.expandedView != null) {
+                ProtoLog.v(WM_SHELL_BUBBLES, "Task initializing expanded view key=%s",
+                        mBubble.getKey());
+                viewInfo.expandedView.initialize(mExpandedViewManager.get(), mStackView.get(),
+                        mPositioner.get(), false /* isOverflow */, viewInfo.taskView);
+            } else if (viewInfo.bubbleBarExpandedView != null) {
+                ProtoLog.v(WM_SHELL_BUBBLES, "Task initializing bubble bar expanded view key=%s",
+                        mBubble.getKey());
+                viewInfo.bubbleBarExpandedView.initialize(mExpandedViewManager.get(),
+                        mPositioner.get(), false /* isOverflow */, viewInfo.taskView);
+            }
+        }
 
-        mMainExecutor.execute(() -> {
-            if (!verifyState()) {
-                return;
-            }
-            mBubble.setViewInfo(viewInfo);
-            if (mCallback != null) {
-                mCallback.onBubbleViewsReady(mBubble);
-            }
-        });
+        mBubble.setViewInfo(viewInfo);
+        if (mCallback != null) {
+            mCallback.onBubbleViewsReady(mBubble);
+        }
     }
 
     private boolean verifyState() {
@@ -158,6 +247,9 @@
     public static class BubbleViewInfo {
         // TODO(b/273312602): for foldables it might make sense to populate all of the views
 
+        // Only set if views where inflated as part of the task
+        @Nullable BubbleTaskView taskView;
+
         // Always populated
         ShortcutInfo shortcutInfo;
         String appName;
@@ -177,9 +269,7 @@
 
         @Nullable
         public static BubbleViewInfo populateForBubbleBar(Context c,
-                BubbleExpandedViewManager expandedViewManager,
                 BubbleTaskViewFactory taskViewFactory,
-                BubblePositioner positioner,
                 BubbleBarLayerView layerView,
                 BubbleIconFactory iconFactory,
                 Bubble b,
@@ -187,12 +277,11 @@
             BubbleViewInfo info = new BubbleViewInfo();
 
             if (!skipInflation && !b.isInflated()) {
-                BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(taskViewFactory);
+                ProtoLog.v(WM_SHELL_BUBBLES, "Task inflating bubble bar views key=%s", b.getKey());
+                info.taskView = b.getOrCreateBubbleTaskView(taskViewFactory);
                 LayoutInflater inflater = LayoutInflater.from(c);
                 info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
                         R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
-                info.bubbleBarExpandedView.initialize(
-                        expandedViewManager, positioner, false /* isOverflow */, bubbleTaskView);
             }
 
             if (!populateCommonInfo(info, c, b, iconFactory)) {
@@ -206,7 +295,6 @@
         @VisibleForTesting
         @Nullable
         public static BubbleViewInfo populate(Context c,
-                BubbleExpandedViewManager expandedViewManager,
                 BubbleTaskViewFactory taskViewFactory,
                 BubblePositioner positioner,
                 BubbleStackView stackView,
@@ -217,17 +305,15 @@
 
             // View inflation: only should do this once per bubble
             if (!skipInflation && !b.isInflated()) {
+                ProtoLog.v(WM_SHELL_BUBBLES, "Task inflating bubble views key=%s", b.getKey());
                 LayoutInflater inflater = LayoutInflater.from(c);
                 info.imageView = (BadgedImageView) inflater.inflate(
                         R.layout.bubble_view, stackView, false /* attachToRoot */);
                 info.imageView.initialize(positioner);
 
-                BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(taskViewFactory);
+                info.taskView = b.getOrCreateBubbleTaskView(taskViewFactory);
                 info.expandedView = (BubbleExpandedView) inflater.inflate(
                         R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
-                info.expandedView.initialize(
-                        expandedViewManager, stackView, positioner, false /* isOverflow */,
-                        bubbleTaskView);
             }
 
             if (!populateCommonInfo(info, c, b, iconFactory)) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index 8035e91..4ac066e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -74,7 +74,6 @@
     private lateinit var bubbleStackView: BubbleStackView
     private lateinit var bubbleBarLayerView: BubbleBarLayerView
     private lateinit var bubblePositioner: BubblePositioner
-    private lateinit var expandedViewManager: BubbleExpandedViewManager
 
     private val bubbleTaskViewFactory = BubbleTaskViewFactory {
         BubbleTaskView(mock<TaskView>(), mock<Executor>())
@@ -155,7 +154,6 @@
                 bubbleController,
                 mainExecutor
             )
-        expandedViewManager = BubbleExpandedViewManager.fromBubbleController(bubbleController)
         bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData)
     }
 
@@ -165,7 +163,6 @@
         val info =
             BubbleViewInfoTask.BubbleViewInfo.populate(
                 context,
-                expandedViewManager,
                 bubbleTaskViewFactory,
                 bubblePositioner,
                 bubbleStackView,
@@ -193,9 +190,7 @@
         val info =
             BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(
                 context,
-                expandedViewManager,
                 bubbleTaskViewFactory,
-                bubblePositioner,
                 bubbleBarLayerView,
                 iconFactory,
                 bubble,
@@ -229,9 +224,7 @@
         val info =
             BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(
                 context,
-                expandedViewManager,
                 bubbleTaskViewFactory,
-                bubblePositioner,
                 bubbleBarLayerView,
                 iconFactory,
                 bubble,
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 1c5049e..3cc0ad2 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -90,23 +90,24 @@
         mDisplayManager = displayManager;
     }
 
-    /**
-     * Register a listener to receive notifications about when the {@link MediaProjection} or
-     * captured content changes state.
-     *
-     * <p>The callback must be registered before invoking
-     * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback,
-     * Handler)} to ensure that any notifications on the callback are not missed. The client must
-     * implement {@link Callback#onStop()} and clean up any resources it is holding, e.g. the
-     * {@link VirtualDisplay} and {@link Surface}.
-     *
-     * @param callback The callback to call.
-     * @param handler  The handler on which the callback should be invoked, or
-     *                 null if the callback should be invoked on the calling thread's looper.
-     * @throws NullPointerException If the given callback is null.
-     * @see #unregisterCallback
-     */
-    public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
+  /**
+   * Register a listener to receive notifications about when the {@link MediaProjection} or captured
+   * content changes state.
+   *
+   * <p>The callback must be registered before invoking {@link #createVirtualDisplay(String, int,
+   * int, int, int, Surface, VirtualDisplay.Callback, Handler)} to ensure that any notifications on
+   * the callback are not missed. The client must implement {@link Callback#onStop()} and clean up
+   * any resources it is holding, e.g. the {@link VirtualDisplay} and {@link Surface}. This should
+   * also update any application UI indicating the MediaProjection status as MediaProjection has
+   * stopped.
+   *
+   * @param callback The callback to call.
+   * @param handler The handler on which the callback should be invoked, or null if the callback
+   *     should be invoked on the calling thread's looper.
+   * @throws NullPointerException If the given callback is null.
+   * @see #unregisterCallback
+   */
+  public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
         try {
             final Callback c = Objects.requireNonNull(callback);
             if (handler == null) {
@@ -158,74 +159,67 @@
         return createVirtualDisplay(builder, callback, handler);
     }
 
-    /**
-     * Creates a {@link android.hardware.display.VirtualDisplay} to capture the
-     * contents of the screen.
-     *
-     * <p>To correctly clean up resources associated with a capture, the application must register a
-     * {@link Callback} before invocation. The app must override {@link Callback#onStop()} to clean
-     * up (by invoking{@link VirtualDisplay#release()}, {@link Surface#release()} and related
-     * resources).
-     *
-     * @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 dpi      The density of the virtual display in dpi. Must be greater than 0.
-     * @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. See {@link DisplayManager} for the
-     *                 full list of flags. Note that
-     *                 {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_PRESENTATION}
-     *                 is always enabled. The following flags may be overridden, depending on how
-     *                 the component with {android.Manifest.permission.MANAGE_MEDIA_PROJECTION}
-     *                 handles the user's consent:
-     *                 {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY},
-     *                 {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR},
-     *                 {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_PUBLIC}.
-     * @param callback Callback invoked when the virtual display's state changes, or null.
-     * @param handler  The {@link android.os.Handler} on which the callback should be invoked, or
-     *                 null if the callback should be invoked on the calling thread's main
-     *                 {@link android.os.Looper}.
-     * @throws IllegalStateException If the target SDK is {@link
-     *                               android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, and
-     *                               if no {@link Callback} is registered.
-     * @throws SecurityException In any of the following scenarios:
-     *                               <ol>
-     *                                 <li>If attempting to create a new virtual display
-     *                                 associated with this MediaProjection instance after it has
-     *                                 been stopped by invoking {@link #stop()}.
-     *                                 <li>If attempting to create a new virtual display
-     *                                 associated with this MediaProjection instance after a
-     *                                 {@link MediaProjection.Callback#onStop()} callback has been
-     *                                 received due to the user or the system stopping the
-     *                                 MediaProjection session.
-     *                                 <li>If the target SDK is {@link
-     *                                 android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up,
-     *                                 and if this instance has already taken a recording through
-     *                                 {@code #createVirtualDisplay}, but {@link #stop()} wasn't
-     *                                 invoked to end the recording.
-     *                                 <li>If the target SDK is {@link
-     *                                 android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up,
-     *                                 and if {@link MediaProjectionManager#getMediaProjection}
-     *                                 was invoked more than once to get this
-     *                                 {@code MediaProjection} instance.
-     *                               </ol>
-     *                               In cases 2 & 3, no exception is thrown if the target SDK is
-     *                               less than
-     *                               {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}.
-     *                               Instead, recording doesn't begin until the user re-grants
-     *                               consent in the dialog.
-     * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay}
-     * could be created.
-     * @see VirtualDisplay
-     * @see VirtualDisplay.Callback
-     */
-    @SuppressWarnings("RequiresPermission")
-    @Nullable
-    public VirtualDisplay createVirtualDisplay(@NonNull String name,
-            int width, int height, int dpi, @VirtualDisplayFlag int flags,
-            @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback,
-            @Nullable Handler handler) {
+  /**
+   * Creates a {@link android.hardware.display.VirtualDisplay} to capture the contents of the
+   * screen.
+   *
+   * <p>To correctly clean up resources associated with a capture, the application must register a
+   * {@link Callback} before invocation. The app must override {@link Callback#onStop()} to clean up
+   * resources (by invoking{@link VirtualDisplay#release()}, {@link Surface#release()} and related
+   * resources) and to update any available UI regarding the MediaProjection status.
+   *
+   * @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 dpi The density of the virtual display in dpi. Must be greater than 0.
+   * @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. See {@link DisplayManager} for the full
+   *     list of flags. Note that {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_PRESENTATION} is always
+   *     enabled. The following flags may be overridden, depending on how the component with
+   *     {android.Manifest.permission.MANAGE_MEDIA_PROJECTION} handles the user's consent: {@link
+   *     DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY}, {@link
+   *     DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR}, {@link
+   *     DisplayManager#VIRTUAL_DISPLAY_FLAG_PUBLIC}.
+   * @param callback Callback invoked when the virtual display's state changes, or null.
+   * @param handler The {@link android.os.Handler} on which the callback should be invoked, or null
+   *     if the callback should be invoked on the calling thread's main {@link android.os.Looper}.
+   * @throws IllegalStateException If the target SDK is {@link
+   *     android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, and if no {@link Callback} is
+   *     registered.
+   * @throws SecurityException In any of the following scenarios:
+   *     <ol>
+   *       <li>If attempting to create a new virtual display associated with this MediaProjection
+   *           instance after it has been stopped by invoking {@link #stop()}.
+   *       <li>If attempting to create a new virtual display associated with this MediaProjection
+   *           instance after a {@link MediaProjection.Callback#onStop()} callback has been received
+   *           due to the user or the system stopping the MediaProjection session.
+   *       <li>If the target SDK is {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and
+   *           up, and if this instance has already taken a recording through {@code
+   *           #createVirtualDisplay}, but {@link #stop()} wasn't invoked to end the recording.
+   *       <li>If the target SDK is {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and
+   *           up, and if {@link MediaProjectionManager#getMediaProjection} was invoked more than
+   *           once to get this {@code MediaProjection} instance.
+   *     </ol>
+   *     In cases 2 & 3, no exception is thrown if the target SDK is less than {@link
+   *     android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}. Instead, recording doesn't begin until
+   *     the user re-grants consent in the dialog.
+   * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay} could
+   *     be created.
+   * @see VirtualDisplay
+   * @see VirtualDisplay.Callback
+   */
+  @SuppressWarnings("RequiresPermission")
+  @Nullable
+  public VirtualDisplay createVirtualDisplay(
+      @NonNull String name,
+      int width,
+      int height,
+      int dpi,
+      @VirtualDisplayFlag int flags,
+      @Nullable Surface surface,
+      @Nullable VirtualDisplay.Callback callback,
+      @Nullable Handler handler) {
         if (shouldMediaProjectionRequireCallback()) {
             if (mCallbacks.isEmpty()) {
                 final IllegalStateException e = new IllegalStateException(
@@ -322,14 +316,20 @@
          * Called when the MediaProjection session is no longer valid.
          *
          * <p>Once a MediaProjection has been stopped, it's up to the application to release any
-         * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and
-         * {@link Surface}).
+         * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and {@link
+         * Surface}). If the application is displaying any UI indicating the MediaProjection state
+         * it should be updated to indicate that MediaProjection is no longer active.
          *
-         * <p>After this callback any call to
-         * {@link MediaProjection#createVirtualDisplay} will fail, even if no such
-         * {@link VirtualDisplay} was ever created for this MediaProjection session.
+         * <p>MediaProjection stopping can be a result of the system stopping the ongoing
+         * MediaProjection due to various reasons, such as another MediaProjection session starting.
+         * MediaProjection may also stop due to the user explicitly stopping ongoing MediaProjection
+         * via any available system-level UI.
+         *
+         * <p>After this callback any call to {@link MediaProjection#createVirtualDisplay} will
+         * fail, even if no such {@link VirtualDisplay} was ever created for this MediaProjection
+         * session.
          */
-        public void onStop() { }
+        public void onStop() {}
 
         /**
          * Invoked immediately after capture begins or when the size of the captured region changes,
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 7a7137a..03fd2c6 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -64,7 +64,9 @@
  *       holding, e.g. the {@link VirtualDisplay} and {@link Surface}. The MediaProjection may
  *       further no longer create any new {@link VirtualDisplay}s via {@link
  *       MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
- *       VirtualDisplay.Callback, Handler)}.
+ *       VirtualDisplay.Callback, Handler)}. Note that the `onStop()` callback can be a result of
+ *       the system stopping MediaProjection due to various reasons or the user stopping the
+ *       MediaProjection via UI affordances in system-level UI.
  *   <li>Start the screen capture session for media projection by calling {@link
  *       MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
  *       android.hardware.display.VirtualDisplay.Callback, Handler)}.
diff --git a/packages/ExternalStorageProvider/TEST_MAPPING b/packages/ExternalStorageProvider/TEST_MAPPING
new file mode 100644
index 0000000..dfa0c84
--- /dev/null
+++ b/packages/ExternalStorageProvider/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+  "postsubmit": [
+    {
+      "name": "ExternalStorageProviderTests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/packages/ExternalStorageProvider/tests/Android.bp b/packages/ExternalStorageProvider/tests/Android.bp
index 86c62ef..097bb860 100644
--- a/packages/ExternalStorageProvider/tests/Android.bp
+++ b/packages/ExternalStorageProvider/tests/Android.bp
@@ -12,6 +12,10 @@
 
     manifest: "AndroidManifest.xml",
 
+    test_suites: [
+        "general-tests",
+    ],
+
     srcs: [
         "src/**/*.java",
     ],
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background.xml
index eba9c2c..9aa0bc3 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background.xml
@@ -15,16 +15,20 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
     <item
-        android:left="?android:attr/listPreferredItemPaddingStart"
-        android:right="?android:attr/listPreferredItemPaddingEnd"
-        android:top="2dp">
+        android:start="?android:attr/listPreferredItemPaddingStart"
+        android:end="?android:attr/listPreferredItemPaddingEnd"
+        android:top="2dp"
+        android:bottom="16dp">
         <shape android:shape="rectangle">
             <solid
-                android:color="@color/settingslib_preference_bg_color" />
+                android:color="@color/settingslib_materialColorSurfaceBright" />
             <corners
                 android:radius="@dimen/settingslib_preference_corner_radius" />
+            <padding
+                android:bottom="16dp"/>
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_bottom.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_bottom.xml
index 5c60f37..554cba5 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_bottom.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_bottom.xml
@@ -15,19 +15,23 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
     <item
-        android:left="?android:attr/listPreferredItemPaddingStart"
-        android:right="?android:attr/listPreferredItemPaddingEnd"
-        android:top="2dp">
+        android:start="?android:attr/listPreferredItemPaddingStart"
+        android:end="?android:attr/listPreferredItemPaddingEnd"
+        android:top="2dp"
+        android:bottom="16dp">
         <shape android:shape="rectangle">
             <solid
-                android:color="@color/settingslib_preference_bg_color" />
+                android:color="@color/settingslib_materialColorSurfaceBright" />
             <corners
                 android:topLeftRadius="4dp"
                 android:bottomLeftRadius="@dimen/settingslib_preference_corner_radius"
                 android:topRightRadius="4dp"
                 android:bottomRightRadius="@dimen/settingslib_preference_corner_radius" />
+            <padding
+                android:bottom="16dp"/>
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_bottom_selected.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_bottom_selected.xml
index de64efd..f4766ee 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_bottom_selected.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_bottom_selected.xml
@@ -15,19 +15,20 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
     <item
-        android:left="?android:attr/listPreferredItemPaddingStart"
-        android:right="?android:attr/listPreferredItemPaddingEnd"
-        android:top="2dp">
+        android:start="?android:attr/listPreferredItemPaddingStart"
+        android:end="?android:attr/listPreferredItemPaddingEnd"
+        android:top="2dp"
+        android:bottom="16dp">
         <shape android:shape="rectangle">
             <solid
-                android:color="@color/settingslib_materialColorSurfaceContainerHigh" />
+                android:color="@color/settingslib_materialColorSurfaceContainer" />
             <corners
-                android:topLeftRadius="4dp"
-                android:bottomLeftRadius="@dimen/settingslib_preference_corner_radius"
-                android:topRightRadius="4dp"
-                android:bottomRightRadius="@dimen/settingslib_preference_corner_radius" />
+                android:radius="@dimen/settingslib_preference_corner_radius_selected" />
+            <padding
+                android:bottom="16dp"/>
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_center.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_center.xml
index dd70f4f..b89a0dd 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_center.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_center.xml
@@ -15,16 +15,17 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
     <item
-        android:left="?android:attr/listPreferredItemPaddingStart"
-        android:right="?android:attr/listPreferredItemPaddingEnd"
+        android:start="?android:attr/listPreferredItemPaddingStart"
+        android:end="?android:attr/listPreferredItemPaddingEnd"
         android:top="2dp">
         <shape android:shape="rectangle">
             <solid
-                android:color="@color/settingslib_preference_bg_color" />
+                android:color="@color/settingslib_materialColorSurfaceBright" />
             <corners
                 android:radius="4dp" />
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_center_selected.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_center_selected.xml
index fffc6c8..40eafc2 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_center_selected.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_center_selected.xml
@@ -15,16 +15,17 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
     <item
-        android:left="?android:attr/listPreferredItemPaddingStart"
-        android:right="?android:attr/listPreferredItemPaddingEnd"
+        android:start="?android:attr/listPreferredItemPaddingStart"
+        android:end="?android:attr/listPreferredItemPaddingEnd"
         android:top="2dp">
         <shape android:shape="rectangle">
             <solid
-                android:color="@color/settingslib_materialColorSurfaceContainerHigh" />
+                android:color="@color/settingslib_materialColorSurfaceContainer" />
             <corners
-                android:radius="4dp" />
+                android:radius="@dimen/settingslib_preference_corner_radius_selected" />
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_selected.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_selected.xml
index f83e3b1..f4766ee 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_selected.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_selected.xml
@@ -15,16 +15,20 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
     <item
-        android:left="?android:attr/listPreferredItemPaddingStart"
-        android:right="?android:attr/listPreferredItemPaddingEnd"
-        android:top="2dp">
+        android:start="?android:attr/listPreferredItemPaddingStart"
+        android:end="?android:attr/listPreferredItemPaddingEnd"
+        android:top="2dp"
+        android:bottom="16dp">
         <shape android:shape="rectangle">
             <solid
-                android:color="@color/settingslib_materialColorSurfaceContainerHigh" />
+                android:color="@color/settingslib_materialColorSurfaceContainer" />
             <corners
-                android:radius="@dimen/settingslib_preference_corner_radius" />
+                android:radius="@dimen/settingslib_preference_corner_radius_selected" />
+            <padding
+                android:bottom="16dp"/>
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_top.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_top.xml
index ab79d18..7955e44 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_top.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_top.xml
@@ -15,14 +15,15 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
     <item
-        android:left="?android:attr/listPreferredItemPaddingStart"
-        android:right="?android:attr/listPreferredItemPaddingEnd"
+        android:start="?android:attr/listPreferredItemPaddingStart"
+        android:end="?android:attr/listPreferredItemPaddingEnd"
         android:top="2dp">
         <shape android:shape="rectangle">
             <solid
-                android:color="@color/settingslib_preference_bg_color" />
+                android:color="@color/settingslib_materialColorSurfaceBright" />
             <corners
                 android:topLeftRadius="@dimen/settingslib_preference_corner_radius"
                 android:bottomLeftRadius="4dp"
@@ -30,4 +31,4 @@
                 android:bottomRightRadius="4dp" />
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_top_selected.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_top_selected.xml
index 112ec73..40eafc2 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_top_selected.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_round_background_top_selected.xml
@@ -15,19 +15,17 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
     <item
-        android:left="?android:attr/listPreferredItemPaddingStart"
-        android:right="?android:attr/listPreferredItemPaddingEnd"
+        android:start="?android:attr/listPreferredItemPaddingStart"
+        android:end="?android:attr/listPreferredItemPaddingEnd"
         android:top="2dp">
         <shape android:shape="rectangle">
             <solid
-                android:color="@color/settingslib_materialColorSurfaceContainerHigh" />
+                android:color="@color/settingslib_materialColorSurfaceContainer" />
             <corners
-                android:topLeftRadius="@dimen/settingslib_preference_corner_radius"
-                android:bottomLeftRadius="4dp"
-                android:topRightRadius="@dimen/settingslib_preference_corner_radius"
-                android:bottomRightRadius="4dp" />
+                android:radius="@dimen/settingslib_preference_corner_radius_selected" />
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_preference_category_no_title.xml b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_preference_category_no_title.xml
index eda7daa..7f466f6 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_preference_category_no_title.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_preference_category_no_title.xml
@@ -18,6 +18,5 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:baselineAligned="false"
-    android:layout_marginTop="16dp">
+    android:baselineAligned="false">
 </LinearLayout>
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/dimens.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/dimens.xml
index d783956..193ae61 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/dimens.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/dimens.xml
@@ -17,4 +17,5 @@
 
 <resources>
     <dimen name="settingslib_preference_corner_radius">20dp</dimen>
+    <dimen name="settingslib_preference_corner_radius_selected">28dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingFooterPreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingFooterPreference.java
new file mode 100644
index 0000000..2099b33
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingFooterPreference.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+/** A data class representing a footer preference. */
+public class DeviceSettingFooterPreference extends DeviceSettingPreference implements Parcelable {
+
+    private final String mFooterText;
+    private final Bundle mExtras;
+
+    DeviceSettingFooterPreference(
+            @NonNull String footerText,
+            Bundle extras) {
+        super(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE);
+        mFooterText = footerText;
+        mExtras = extras;
+    }
+
+    /** Read a {@link DeviceSettingFooterPreference} from {@link Parcel}. */
+    @NonNull
+    public static DeviceSettingFooterPreference readFromParcel(@NonNull Parcel in) {
+        String footerText = in.readString();
+        Bundle extras = in.readBundle(Bundle.class.getClassLoader());
+        return new DeviceSettingFooterPreference(footerText, extras);
+    }
+
+    public static final Creator<DeviceSettingFooterPreference> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public DeviceSettingFooterPreference createFromParcel(@NonNull Parcel in) {
+                    in.readInt();
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public DeviceSettingFooterPreference[] newArray(int size) {
+                    return new DeviceSettingFooterPreference[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString(mFooterText);
+        dest.writeBundle(mExtras);
+    }
+
+    /** Builder class for {@link DeviceSettingFooterPreference}. */
+    public static final class Builder {
+        private String mFooterText = "";
+        private Bundle mExtras = Bundle.EMPTY;
+
+        /**
+         * Sets the footer text of the preference.
+         *
+         * @param footerText The footer text of the preference.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public DeviceSettingFooterPreference.Builder setFooterText(@NonNull String footerText) {
+            mFooterText = footerText;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public DeviceSettingFooterPreference.Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * Builds the {@link DeviceSettingFooterPreference} object.
+         *
+         * @return Returns the built {@link DeviceSettingFooterPreference} object.
+         */
+        @NonNull
+        public DeviceSettingFooterPreference build() {
+            return new DeviceSettingFooterPreference(
+                    mFooterText, mExtras);
+        }
+    }
+
+    /**
+     * Gets the footer text of the preference.
+     *
+     * @return The footer text.
+     */
+    @NonNull
+    public String getFooterText() {
+        return mFooterText;
+    }
+
+    /**
+     * Gets the extras Bundle.
+     *
+     * @return Returns a Bundle object.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java
index 790939a..4b67ef7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java
@@ -40,6 +40,8 @@
                 return ActionSwitchPreference.readFromParcel(in);
             case DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE:
                 return MultiTogglePreference.readFromParcel(in);
+            case DeviceSettingType.DEVICE_SETTING_TYPE_FOOTER:
+                return DeviceSettingFooterPreference.readFromParcel(in);
             default:
                 return UNKNOWN;
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java
index ee4d90f..441e3f8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java
@@ -27,6 +27,7 @@
             DeviceSettingType.DEVICE_SETTING_TYPE_UNKNOWN,
             DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH,
             DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE,
+            DeviceSettingType.DEVICE_SETTING_TYPE_FOOTER,
         },
         open = true)
 public @interface DeviceSettingType {
@@ -38,4 +39,7 @@
 
     /** Device setting type is multi-toggle preference. */
     int DEVICE_SETTING_TYPE_MULTI_TOGGLE = 2;
+
+    /** Device setting type is footer preference. */
+    int DEVICE_SETTING_TYPE_FOOTER = 3;
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt
index c8a2e9c..127275f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt
@@ -31,7 +31,6 @@
 data class DeviceSettingsConfig(
     val mainContentItems: List<DeviceSettingItem>,
     val moreSettingsItems: List<DeviceSettingItem>,
-    val moreSettingsFooter: String,
     val extras: Bundle = Bundle.EMPTY,
 ) : Parcelable {
 
@@ -41,7 +40,6 @@
         parcel.run {
             writeTypedList(mainContentItems)
             writeTypedList(moreSettingsItems)
-            writeString(moreSettingsFooter)
             writeBundle(extras)
         }
     }
@@ -61,7 +59,6 @@
                                 arrayListOf<DeviceSettingItem>().also {
                                     readTypedList(it, DeviceSettingItem.CREATOR)
                                 },
-                            moreSettingsFooter = readString()!!,
                             extras = readBundle((Bundle::class.java.classLoader))!!,
                         )
                     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
index ce7064c..cded014 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
@@ -25,6 +25,7 @@
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingFooterPreference
 import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference
 import com.android.settingslib.bluetooth.devicesettings.ToggleInfo
 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
@@ -96,8 +97,7 @@
     private fun DeviceSettingsConfig.toModel(): DeviceSettingConfigModel =
         DeviceSettingConfigModel(
             mainItems = mainContentItems.map { it.toModel() },
-            moreSettingsItems = moreSettingsItems.map { it.toModel() },
-            moreSettingsPageFooter = moreSettingsFooter)
+            moreSettingsItems = moreSettingsItems.map { it.toModel() })
 
     private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel {
         return if (!TextUtils.isEmpty(preferenceKey)) {
@@ -151,6 +151,9 @@
                         }
                     },
                 )
+            is DeviceSettingFooterPreference -> DeviceSettingModel.FooterPreference(
+                cachedDevice = cachedDevice,
+                id = settingId, footerText = pref.footerText)
             else -> DeviceSettingModel.Unknown(cachedDevice, settingId)
         }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
index e97f76c..4062462 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
@@ -24,8 +24,6 @@
     val mainItems: List<DeviceSettingConfigItemModel>,
     /** Items need to be shown in device details more settings page. */
     val moreSettingsItems: List<DeviceSettingConfigItemModel>,
-    /** Footer text in more settings page. */
-    val moreSettingsPageFooter: String
 )
 
 /** Models a device setting item in config. */
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt
index 2a63217..5fd4d06 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt
@@ -52,6 +52,13 @@
         val updateState: (DeviceSettingStateModel.MultiTogglePreferenceState) -> Unit
     ) : DeviceSettingModel
 
+    /** Models a footer preference. */
+    data class FooterPreference(
+        override val cachedDevice: CachedBluetoothDevice,
+        @DeviceSettingId override val id: Int,
+        val footerText: String,
+    ) : DeviceSettingModel
+
     /** Models an unknown preference. */
     data class Unknown(
         override val cachedDevice: CachedBluetoothDevice,
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
index 0d4ce5b..c13b261 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -117,6 +117,21 @@
             .thenComparing(ZenMode::getType, PRIORITIZED_TYPE_COMPARATOR)
             .thenComparing(ZenMode::getName);
 
+    public enum Kind {
+        /** A "normal" mode, created by apps or users via {@code addAutomaticZenRule()}. */
+        NORMAL,
+
+        /** The special, built-in "Do Not Disturb" mode. */
+        MANUAL_DND,
+
+        /**
+         * An implicit mode, automatically created and managed by the system on behalf of apps that
+         * call {@code setInterruptionFilter()} or {@code setNotificationPolicy()} (with some
+         * exceptions).
+         */
+        IMPLICIT,
+    }
+
     public enum Status {
         ENABLED,
         ENABLED_AND_ACTIVE,
@@ -126,8 +141,8 @@
 
     private final String mId;
     private final AutomaticZenRule mRule;
+    private final Kind mKind;
     private final Status mStatus;
-    private final boolean mIsManualDnd;
 
     /**
      * Initializes a {@link ZenMode}, mainly based on the information from the
@@ -137,9 +152,11 @@
      * active, or the reason it was disabled) are read from the {@link ZenModeConfig.ZenRule} --
      * see {@link #computeStatus}.
      */
-    public ZenMode(String id, @NonNull AutomaticZenRule rule,
+    ZenMode(String id, @NonNull AutomaticZenRule rule,
             @NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
-        this(id, rule, computeStatus(zenRuleExtraData), false);
+        this(id, rule,
+                ZenModeConfig.isImplicitRuleId(id) ? Kind.IMPLICIT : Kind.NORMAL,
+                computeStatus(zenRuleExtraData));
     }
 
     private static Status computeStatus(@NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
@@ -158,13 +175,16 @@
         }
     }
 
-    public static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
+    static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
         // Manual rule is owned by the system, so we set it here
         AutomaticZenRule manualRuleWithPkg = new AutomaticZenRule.Builder(manualRule)
                 .setPackage(PACKAGE_ANDROID)
                 .build();
-        return new ZenMode(MANUAL_DND_MODE_ID, manualRuleWithPkg,
-                isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED, true);
+        return new ZenMode(
+                MANUAL_DND_MODE_ID,
+                manualRuleWithPkg,
+                Kind.MANUAL_DND,
+                isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED);
     }
 
     /**
@@ -183,19 +203,19 @@
                 .setIconResId(iconResId)
                 .setManualInvocationAllowed(true)
                 .build();
-        return new ZenMode(TEMP_NEW_MODE_ID, rule, Status.ENABLED, false);
+        return new ZenMode(TEMP_NEW_MODE_ID, rule, Kind.NORMAL, Status.ENABLED);
     }
 
-    private ZenMode(String id, @NonNull AutomaticZenRule rule, Status status, boolean isManualDnd) {
+    private ZenMode(String id, @NonNull AutomaticZenRule rule, Kind kind, Status status) {
         mId = id;
         mRule = rule;
+        mKind = kind;
         mStatus = status;
-        mIsManualDnd = isManualDnd;
     }
 
     /** Creates a deep copy of this object. */
     public ZenMode copy() {
-        return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mStatus, mIsManualDnd);
+        return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mKind, mStatus);
     }
 
     @NonNull
@@ -264,10 +284,32 @@
         return mRule.getType() + ":" + mRule.getPackageName() + ":" + mRule.getIconResId();
     }
 
+    /**
+     * Returns the mode icon -- which can be either app-provided (via {@code addAutomaticZenRule}),
+     * user-chosen (via the icon picker in Settings), the app's launcher icon for implicit rules
+     * (in its monochrome variant, if available), or a default icon based on the mode type.
+     */
     @NonNull
     public ListenableFuture<Drawable> getIcon(@NonNull Context context,
             @NonNull ZenIconLoader iconLoader) {
-        if (mIsManualDnd) {
+        if (mKind == Kind.MANUAL_DND) {
+            return Futures.immediateFuture(requireNonNull(
+                    context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
+        }
+
+        return iconLoader.getIcon(context, mRule);
+    }
+
+    /**
+     * Returns an alternative mode icon. The difference with {@link #getIcon} is that it's the
+     * basic DND icon not only for Manual DND, but also for <em>implicit rules</em>. As such, it's
+     * suitable for places where showing the launcher icon of an app could be confusing, such as
+     * the status bar or lockscreen.
+     */
+    @NonNull
+    public ListenableFuture<Drawable> getLockscreenIcon(@NonNull Context context,
+            @NonNull ZenIconLoader iconLoader) {
+        if (mKind == Kind.MANUAL_DND || mKind == Kind.IMPLICIT) {
             return Futures.immediateFuture(requireNonNull(
                     context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
         }
@@ -373,7 +415,7 @@
     }
 
     public boolean isManualDnd() {
-        return mIsManualDnd;
+        return mKind == Kind.MANUAL_DND;
     }
 
     /**
@@ -404,18 +446,18 @@
         return obj instanceof ZenMode other
                 && mId.equals(other.mId)
                 && mRule.equals(other.mRule)
-                && mStatus.equals(other.mStatus)
-                && mIsManualDnd == other.mIsManualDnd;
+                && mKind.equals(other.mKind)
+                && mStatus.equals(other.mStatus);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mId, mRule, mStatus, mIsManualDnd);
+        return Objects.hash(mId, mRule, mKind, mStatus);
     }
 
     @Override
     public String toString() {
-        return mId + " (" + mStatus + ") -> " + mRule;
+        return mId + " (" + mKind + ", " + mStatus + ") -> " + mRule;
     }
 
     @Override
@@ -427,8 +469,8 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString(mId);
         dest.writeParcelable(mRule, 0);
+        dest.writeString(mKind.name());
         dest.writeString(mStatus.name());
-        dest.writeBoolean(mIsManualDnd);
     }
 
     public static final Creator<ZenMode> CREATOR = new Creator<ZenMode>() {
@@ -438,8 +480,8 @@
                     in.readString(),
                     checkNotNull(in.readParcelable(AutomaticZenRule.class.getClassLoader(),
                             AutomaticZenRule.class)),
-                    Status.valueOf(in.readString()),
-                    in.readBoolean());
+                    Kind.valueOf(in.readString()),
+                    Status.valueOf(in.readString()));
         }
 
         @Override
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingFooterPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingFooterPreferenceTest.java
new file mode 100644
index 0000000..cc2f788
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingFooterPreferenceTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class DeviceSettingFooterPreferenceTest {
+
+    @Test
+    public void getMethods() {
+        DeviceSettingFooterPreference preference =
+                new DeviceSettingFooterPreference.Builder()
+                        .setFooterText("footer_text")
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        assertThat(preference.getFooterText()).isEqualTo("footer_text");
+        assertThat(preference.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void parcelOperation() {
+        DeviceSettingFooterPreference preference =
+                new DeviceSettingFooterPreference.Builder()
+                        .setFooterText("footer_text")
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        DeviceSettingFooterPreference fromParcel = writeAndRead(preference);
+
+        assertThat(fromParcel.getFooterText()).isEqualTo(preference.getFooterText());
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(preference.getExtras().getString("key1"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private DeviceSettingFooterPreference writeAndRead(DeviceSettingFooterPreference preference) {
+        Parcel parcel = Parcel.obtain();
+        preference.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        DeviceSettingFooterPreference fromParcel =
+                DeviceSettingFooterPreference.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
index 9568d66..7223e90 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
@@ -50,7 +50,6 @@
                             null,
                             Bundle(),
                         )),
-                moreSettingsFooter = "footer",
                 extras = Bundle().apply { putString("key1", "value1") },
             )
 
@@ -72,7 +71,6 @@
             .containsExactly("class_name_2")
         assertThat(fromParcel.moreSettingsItems.stream().map { it.intentAction }.toList())
             .containsExactly("intent_action_2")
-        assertThat(fromParcel.moreSettingsFooter).isEqualTo(config.moreSettingsFooter)
     }
 
     private fun writeAndRead(item: DeviceSettingsConfig): DeviceSettingsConfig {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index 4c5ee9e..061d515 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -97,9 +97,7 @@
         `when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS)
         `when`(
                 bluetoothDevice.getMetadata(
-                    DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
-                )
-            )
+                    DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
             .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray())
 
         `when`(configService.queryLocalInterface(anyString())).thenReturn(configService)
@@ -122,8 +120,7 @@
                     connection.onServiceConnected(
                         ComponentName(
                             SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1,
-                            SETTING_PROVIDER_SERVICE_CLASS_NAME_1
-                        ),
+                            SETTING_PROVIDER_SERVICE_CLASS_NAME_1),
                         settingProviderService1,
                     )
                 SETTING_PROVIDER_SERVICE_INTENT_ACTION_2 ->
@@ -168,9 +165,7 @@
             `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
             `when`(
                     bluetoothDevice.getMetadata(
-                        DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
-                    )
-                )
+                        DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
                 .thenReturn("".toByteArray())
 
             var config: DeviceSettingConfigModel? = null
@@ -178,10 +173,7 @@
             delay(1000)
             verify(bluetoothAdapter)
                 .addOnMetadataChangedListener(
-                    eq(bluetoothDevice),
-                    any(),
-                    metadataChangeCaptor.capture()
-                )
+                    eq(bluetoothDevice), any(), metadataChangeCaptor.capture())
             metadataChangeCaptor.value.onMetadataChanged(
                 bluetoothDevice,
                 DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
@@ -189,9 +181,7 @@
             )
             `when`(
                     bluetoothDevice.getMetadata(
-                        DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
-                    )
-                )
+                        DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
                 .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray())
 
             job.join()
@@ -303,10 +293,8 @@
                     DeviceSettingState.Builder()
                         .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER)
                         .setPreferenceState(
-                            ActionSwitchPreferenceState.Builder().setChecked(false).build()
-                        )
-                        .build()
-                )
+                            ActionSwitchPreferenceState.Builder().setChecked(false).build())
+                        .build())
         }
     }
 
@@ -337,10 +325,8 @@
                     DeviceSettingState.Builder()
                         .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC)
                         .setPreferenceState(
-                            MultiTogglePreferenceState.Builder().setState(2).build()
-                        )
-                        .build()
-                )
+                            MultiTogglePreferenceState.Builder().setState(2).build())
+                        .build())
         }
     }
 
@@ -353,7 +339,8 @@
                 val pref = serviceResponse.preference as ActionSwitchPreference
                 assertThat(actual.title).isEqualTo(pref.title)
                 assertThat(actual.summary).isEqualTo(pref.summary)
-                assertThat(actual.icon).isEqualTo(DeviceSettingIcon.BitmapIcon(pref.icon!!))
+                assertThat(actual.icon)
+                    .isEqualTo(pref.icon?.let { DeviceSettingIcon.BitmapIcon(it) })
                 assertThat(actual.isAllowedChangingState).isEqualTo(pref.isAllowedChangingState)
                 if (pref.hasSwitch()) {
                     assertThat(actual.switchState!!.checked).isEqualTo(pref.checked)
@@ -378,7 +365,8 @@
 
     private fun assertToggle(actual: ToggleModel, serviceResponse: ToggleInfo) {
         assertThat(actual.label).isEqualTo(serviceResponse.label)
-        assertThat(actual.icon).isEqualTo(serviceResponse.icon)
+        assertThat((actual.icon as DeviceSettingIcon.BitmapIcon).bitmap)
+            .isEqualTo(serviceResponse.icon)
     }
 
     private fun assertConfig(
@@ -393,7 +381,6 @@
         for (i in 0..<actual.moreSettingsItems.size) {
             assertConfigItem(actual.moreSettingsItems[i], serviceResponse.moreSettingsItems[i])
         }
-        assertThat(actual.moreSettingsPageFooter).isEqualTo(serviceResponse.moreSettingsFooter)
     }
 
     private fun assertConfigItem(
@@ -437,15 +424,13 @@
                 DeviceSettingId.DEVICE_SETTING_ID_HEADER,
                 SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1,
                 SETTING_PROVIDER_SERVICE_CLASS_NAME_1,
-                SETTING_PROVIDER_SERVICE_INTENT_ACTION_1
-            )
+                SETTING_PROVIDER_SERVICE_INTENT_ACTION_1)
         val DEVICE_SETTING_ITEM_2 =
             DeviceSettingItem(
                 DeviceSettingId.DEVICE_SETTING_ID_ANC,
                 SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2,
                 SETTING_PROVIDER_SERVICE_CLASS_NAME_2,
-                SETTING_PROVIDER_SERVICE_INTENT_ACTION_2
-            )
+                SETTING_PROVIDER_SERVICE_INTENT_ACTION_2)
         val DEVICE_SETTING_1 =
             DeviceSetting.Builder()
                 .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER)
@@ -454,8 +439,7 @@
                         .setTitle("title1")
                         .setHasSwitch(true)
                         .setAllowedChangingState(true)
-                        .build()
-                )
+                        .build())
                 .build()
         val DEVICE_SETTING_2 =
             DeviceSetting.Builder()
@@ -468,22 +452,18 @@
                             ToggleInfo.Builder()
                                 .setLabel("label1")
                                 .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
-                                .build()
-                        )
+                                .build())
                         .addToggleInfo(
                             ToggleInfo.Builder()
                                 .setLabel("label2")
                                 .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
-                                .build()
-                        )
-                        .build()
-                )
+                                .build())
+                        .build())
                 .build()
         val DEVICE_SETTING_CONFIG =
             DeviceSettingsConfig(
                 listOf(DEVICE_SETTING_ITEM_1),
                 listOf(DEVICE_SETTING_ITEM_2),
-                "footer"
             )
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
index bab4bc3b..f533e77 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -30,7 +30,14 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
 import android.app.AutomaticZenRule;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Parcel;
 import android.service.notification.Condition;
@@ -40,9 +47,12 @@
 
 import com.android.internal.R;
 
+import com.google.common.util.concurrent.ListenableFuture;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -60,6 +70,13 @@
                     .setZenPolicy(ZEN_POLICY)
                     .build();
 
+    private static final String IMPLICIT_RULE_ID = ZenModeConfig.implicitRuleId("some.package");
+    private static final AutomaticZenRule IMPLICIT_ZEN_RULE =
+            new AutomaticZenRule.Builder("Implicit", Uri.parse("implicit/some.package"))
+                    .setPackage("some.package")
+                    .setType(TYPE_OTHER)
+                    .build();
+
     @Test
     public void testBasicMethods() {
         ZenMode zenMode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, true));
@@ -265,6 +282,79 @@
 
         assertUnparceledIsEqualToOriginal("custom_manual",
                 ZenMode.newCustomManual("New mode", R.drawable.ic_zen_mode_type_immersive));
+
+        assertUnparceledIsEqualToOriginal("implicit",
+                new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
+                        zenConfigRuleFor(IMPLICIT_ZEN_RULE, false)));
+    }
+
+    @Test
+    public void getIcon_normalMode_loadsIconNormally() {
+        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+        ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false));
+
+        ListenableFuture<Drawable> unused = mode.getIcon(RuntimeEnvironment.getApplication(),
+                iconLoader);
+
+        verify(iconLoader).getIcon(any(), eq(ZEN_RULE));
+    }
+
+    @Test
+    public void getIcon_manualDnd_returnsFixedIcon() {
+        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+
+        ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getIcon(
+                RuntimeEnvironment.getApplication(), iconLoader);
+
+        assertThat(future.isDone()).isTrue();
+        verify(iconLoader, never()).getIcon(any(), any());
+    }
+
+    @Test
+    public void getIcon_implicitMode_loadsIconNormally() {
+        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+        ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
+                zenConfigRuleFor(IMPLICIT_ZEN_RULE, false));
+
+        ListenableFuture<Drawable> unused = mode.getIcon(RuntimeEnvironment.getApplication(),
+                iconLoader);
+
+        verify(iconLoader).getIcon(any(), eq(IMPLICIT_ZEN_RULE));
+    }
+
+    @Test
+    public void getLockscreenIcon_normalMode_loadsIconNormally() {
+        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+        ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false));
+
+        ListenableFuture<Drawable> unused = mode.getLockscreenIcon(
+                RuntimeEnvironment.getApplication(), iconLoader);
+
+        verify(iconLoader).getIcon(any(), eq(ZEN_RULE));
+    }
+
+    @Test
+    public void getLockscreenIcon_manualDnd_returnsFixedIcon() {
+        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+
+        ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getLockscreenIcon(
+                RuntimeEnvironment.getApplication(), iconLoader);
+
+        assertThat(future.isDone()).isTrue();
+        verify(iconLoader, never()).getIcon(any(), any());
+    }
+
+    @Test
+    public void getLockscreenIcon_implicitMode_returnsFixedIcon() {
+        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
+        ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
+                zenConfigRuleFor(IMPLICIT_ZEN_RULE, false));
+
+        ListenableFuture<Drawable> future = mode.getLockscreenIcon(
+                RuntimeEnvironment.getApplication(), iconLoader);
+
+        assertThat(future.isDone()).isTrue();
+        verify(iconLoader, never()).getIcon(any(), any());
     }
 
     private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) {
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 3aa89ee..c1bb55c 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -893,6 +893,7 @@
     ],
     static_libs: [
         "RoboTestLibraries",
+        "androidx.compose.runtime_runtime",
     ],
     libs: [
         "android.test.runner",
@@ -929,6 +930,7 @@
     ],
     static_libs: [
         "RoboTestLibraries",
+        "androidx.compose.runtime_runtime",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 3895595..6412276 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -62,7 +62,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -325,13 +324,4 @@
         // enabled.
         mController.onViewAttached();
     }
-
-    @Test
-    public void destroy_cleansUpState() {
-        mController.destroy();
-        verify(mStateController).removeCallback(any());
-        verify(mAmbientStatusBarViewController).destroy();
-        verify(mComplicationHostViewController).destroy();
-        verify(mLowLightTransitionCoordinator).setLowLightEnterListener(ArgumentMatchers.isNull());
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index da82b5f..89ec3cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -596,9 +596,6 @@
         // are created.
         verify(mDreamOverlayComponent).getDreamOverlayContainerViewController()
         verify(mAmbientTouchComponent).getTouchMonitor()
-
-        // Verify DreamOverlayContainerViewController is destroyed.
-        verify(mDreamOverlayContainerViewController).destroy()
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
index bc142e6..6395448 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
@@ -16,120 +16,108 @@
 
 package com.android.systemui.gesture.domain
 
+import android.app.ActivityManager
 import android.content.ComponentName
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigationbar.gestural.data.gestureRepository
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+import com.android.systemui.shared.system.activityManagerWrapper
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.test.setMain
-import org.junit.After
-import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.any
-import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.verify
+import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class GestureInteractorTest : SysuiTestCase() {
     @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val kosmos = testKosmos()
 
-    val dispatcher = StandardTestDispatcher()
+    val dispatcher = kosmos.testDispatcher
+    val repository = spy(kosmos.gestureRepository)
     val testScope = TestScope(dispatcher)
 
-    @Mock private lateinit var gestureRepository: GestureRepository
+    private val underTest by lazy { createInteractor() }
 
-    private val underTest by lazy {
-        GestureInteractor(gestureRepository, testScope.backgroundScope)
+    private fun createInteractor(): GestureInteractor {
+        return GestureInteractor(
+            repository,
+            dispatcher,
+            kosmos.backgroundCoroutineContext,
+            testScope,
+            kosmos.activityManagerWrapper,
+            kosmos.taskStackChangeListeners
+        )
     }
 
-    @Before
-    fun setup() {
-        Dispatchers.setMain(dispatcher)
-        whenever(gestureRepository.gestureBlockedActivities).thenReturn(MutableStateFlow(setOf()))
-    }
+    private fun setTopActivity(componentName: ComponentName) {
+        val task = mock<ActivityManager.RunningTaskInfo>()
+        task.topActivity = componentName
+        whenever(kosmos.activityManagerWrapper.runningTask).thenReturn(task)
 
-    @After
-    fun tearDown() {
-        Dispatchers.resetMain()
+        kosmos.taskStackChangeListeners.listenerImpl.onTaskStackChanged()
     }
 
     @Test
     fun addBlockedActivity_testCombination() =
         testScope.runTest {
             val globalComponent = mock<ComponentName>()
-            whenever(gestureRepository.gestureBlockedActivities)
-                .thenReturn(MutableStateFlow(setOf(globalComponent)))
+            repository.addGestureBlockedActivity(globalComponent)
+
             val localComponent = mock<ComponentName>()
+
+            val blocked by collectLastValue(underTest.topActivityBlocked)
+
             underTest.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local)
-            val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
-            testScope.runCurrent()
-            verify(gestureRepository, never()).addGestureBlockedActivity(any())
-            assertThat(lastSeen).hasSize(2)
-            assertThat(lastSeen).containsExactly(globalComponent, localComponent)
+
+            assertThat(blocked).isFalse()
+
+            setTopActivity(localComponent)
+
+            assertThat(blocked).isTrue()
+        }
+
+    @Test
+    fun initialization_testEmit() =
+        testScope.runTest {
+            val globalComponent = mock<ComponentName>()
+            repository.addGestureBlockedActivity(globalComponent)
+            setTopActivity(globalComponent)
+
+            val interactor = createInteractor()
+
+            val blocked by collectLastValue(interactor.topActivityBlocked)
+            assertThat(blocked).isTrue()
         }
 
     @Test
     fun addBlockedActivityLocally_onlyAffectsLocalInteractor() =
         testScope.runTest {
-            val component = mock<ComponentName>()
-            underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
-            val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
-            testScope.runCurrent()
-            verify(gestureRepository, never()).addGestureBlockedActivity(any())
-            assertThat(lastSeen).contains(component)
-        }
+            val interactor1 = createInteractor()
+            val interactor1Blocked by collectLastValue(interactor1.topActivityBlocked)
+            val interactor2 = createInteractor()
+            val interactor2Blocked by collectLastValue(interactor2.topActivityBlocked)
 
-    @Test
-    fun removeBlockedActivityLocally_onlyAffectsLocalInteractor() =
-        testScope.runTest {
-            val component = mock<ComponentName>()
-            underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
-            val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
-            testScope.runCurrent()
-            underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Local)
-            testScope.runCurrent()
-            verify(gestureRepository, never()).removeGestureBlockedActivity(any())
-            assertThat(lastSeen).isEmpty()
-        }
+            val localComponent = mock<ComponentName>()
 
-    @Test
-    fun addBlockedActivity_invokesRepository() =
-        testScope.runTest {
-            val component = mock<ComponentName>()
-            underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Global)
-            runCurrent()
-            val captor = argumentCaptor<ComponentName>()
-            verify(gestureRepository).addGestureBlockedActivity(captor.capture())
-            assertThat(captor.firstValue).isEqualTo(component)
-        }
+            interactor1.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local)
+            setTopActivity(localComponent)
 
-    @Test
-    fun removeBlockedActivity_invokesRepository() =
-        testScope.runTest {
-            val component = mock<ComponentName>()
-            underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Global)
-            runCurrent()
-            val captor = argumentCaptor<ComponentName>()
-            verify(gestureRepository).removeGestureBlockedActivity(captor.capture())
-            assertThat(captor.firstValue).isEqualTo(component)
+            assertThat(interactor1Blocked).isTrue()
+            assertThat(interactor2Blocked).isFalse()
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
index 976dc52..7d57220 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
@@ -17,8 +17,14 @@
 package com.android.systemui.lifecycle
 
 import android.view.View
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Text
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -26,6 +32,8 @@
 import com.android.systemui.util.Assert
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -149,6 +157,48 @@
 
         assertThat(viewModel.isActivated).isTrue()
     }
+
+    @Test
+    fun hydratedStateOf() {
+        val keepAliveMutable = mutableStateOf(true)
+        val upstreamStateFlow = MutableStateFlow(true)
+        val upstreamFlow = upstreamStateFlow.map { !it }
+        composeRule.setContent {
+            val keepAlive by keepAliveMutable
+            if (keepAlive) {
+                val viewModel = rememberViewModel {
+                    FakeSysUiViewModel(
+                        upstreamFlow = upstreamFlow,
+                        upstreamStateFlow = upstreamStateFlow,
+                    )
+                }
+
+                Column {
+                    Text(
+                        "upstreamStateFlow=${viewModel.stateBackedByStateFlow}",
+                        Modifier.testTag("upstreamStateFlow")
+                    )
+                    Text(
+                        "upstreamFlow=${viewModel.stateBackedByFlow}",
+                        Modifier.testTag("upstreamFlow")
+                    )
+                }
+            }
+        }
+
+        composeRule.waitForIdle()
+        composeRule
+            .onNode(hasTestTag("upstreamStateFlow"))
+            .assertTextEquals("upstreamStateFlow=true")
+        composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=false")
+
+        composeRule.runOnUiThread { upstreamStateFlow.value = false }
+        composeRule.waitForIdle()
+        composeRule
+            .onNode(hasTestTag("upstreamStateFlow"))
+            .assertTextEquals("upstreamStateFlow=false")
+        composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=true")
+    }
 }
 
 private class FakeViewModel : SysUiViewModel() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
index 54b7d25..827236c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
@@ -152,7 +152,7 @@
             }
             with(tiles?.elementAt(1)!!) {
                 assertThat(this.text).isEqualTo("Active with manual")
-                assertThat(this.subtext).isEqualTo("trigger description")
+                assertThat(this.subtext).isEqualTo("On • trigger description")
                 assertThat(this.enabled).isEqualTo(true)
             }
             with(tiles?.elementAt(2)!!) {
@@ -274,6 +274,123 @@
         }
 
     @Test
+    fun tiles_calculatesSubtext() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            repository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setName("With description, inactive")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription("When the going gets tough")
+                        .setActive(false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("With description, active")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription("When in Rome")
+                        .setActive(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("With description, needs setup")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription("When you find yourself in a hole")
+                        .setEnabled(false, /* byUser= */ false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Without description, inactive")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription(null)
+                        .setActive(false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Without description, active")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription(null)
+                        .setActive(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Without description, needs setup")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription(null)
+                        .setEnabled(false, /* byUser= */ false)
+                        .build(),
+                )
+            )
+            runCurrent()
+
+            assertThat(tiles!!).hasSize(6)
+            assertThat(tiles!![0].subtext).isEqualTo("When the going gets tough")
+            assertThat(tiles!![1].subtext).isEqualTo("On • When in Rome")
+            assertThat(tiles!![2].subtext).isEqualTo("Set up")
+            assertThat(tiles!![3].subtext).isEqualTo("Off")
+            assertThat(tiles!![4].subtext).isEqualTo("On")
+            assertThat(tiles!![5].subtext).isEqualTo("Set up")
+        }
+
+    @Test
+    fun tiles_calculatesContentDescription() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            repository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setName("With description, inactive")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription("When the going gets tough")
+                        .setActive(false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("With description, active")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription("When in Rome")
+                        .setActive(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("With description, needs setup")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription("When you find yourself in a hole")
+                        .setEnabled(false, /* byUser= */ false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Without description, inactive")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription(null)
+                        .setActive(false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Without description, active")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription(null)
+                        .setActive(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Without description, needs setup")
+                        .setManualInvocationAllowed(true)
+                        .setTriggerDescription(null)
+                        .setEnabled(false, /* byUser= */ false)
+                        .build(),
+                )
+            )
+            runCurrent()
+
+            assertThat(tiles!!).hasSize(6)
+            assertThat(tiles!![0].contentDescription)
+                .isEqualTo("With description, inactive\nOff\nWhen the going gets tough")
+            assertThat(tiles!![1].contentDescription)
+                .isEqualTo("With description, active\nOn\nWhen in Rome")
+            assertThat(tiles!![2].contentDescription)
+                .isEqualTo("With description, needs setup\nSet up")
+            assertThat(tiles!![3].contentDescription)
+                .isEqualTo("Without description, inactive\nOff")
+            assertThat(tiles!![4].contentDescription).isEqualTo("Without description, active\nOn")
+            assertThat(tiles!![5].contentDescription)
+                .isEqualTo("Without description, needs setup\nSet up")
+        }
+
+    @Test
     fun onClick_togglesTileState() =
         testScope.runTest {
             val tiles by collectLastValue(underTest.tiles)
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8a2e767..18b7073 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1106,6 +1106,9 @@
     <!-- Priority modes: label for an active mode [CHAR LIMIT=35] -->
     <string name="zen_mode_on">On</string>
 
+    <!-- Priority modes: label for an active mode, with details [CHAR LIMIT=10] -->
+    <string name="zen_mode_on_with_details">On • <xliff:g id="trigger_description" example="Mon-Fri, 23:00-7:00">%1$s</xliff:g></string>
+
     <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] -->
     <string name="zen_mode_off">Off</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
index d27e72a..190bc15 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
@@ -122,9 +122,4 @@
      * @param session
      */
     void onSessionStart(TouchSession session);
-
-    /**
-     * Called when the handler is being torn down.
-     */
-    default void onDestroy() {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index 1be6f9e..efa55e9 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -581,10 +581,6 @@
             mBoundsFlow.cancel(new CancellationException());
         }
 
-        for (TouchHandler handler : mHandlers) {
-            handler.onDestroy();
-        }
-
         mInitialized = false;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index 24ac542..b45ebd8 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -44,7 +44,6 @@
 import com.android.systemui.statusbar.CrossFadeHelper
 import javax.inject.Inject
 import javax.inject.Named
-import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.launch
 
 /** Controller for dream overlay animations. */
@@ -85,62 +84,51 @@
 
     private var mCurrentBlurRadius: Float = 0f
 
-    private var mLifecycleFlowHandle: DisposableHandle? = null
-
     fun init(view: View) {
         this.view = view
 
-        mLifecycleFlowHandle =
-            view.repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.CREATED) {
-                    launch {
-                        dreamViewModel.dreamOverlayTranslationY.collect { px ->
-                            ComplicationLayoutParams.iteratePositions(
-                                { position: Int ->
-                                    setElementsTranslationYAtPosition(px, position)
-                                },
-                                POSITION_TOP or POSITION_BOTTOM
-                            )
-                        }
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    dreamViewModel.dreamOverlayTranslationY.collect { px ->
+                        ComplicationLayoutParams.iteratePositions(
+                            { position: Int -> setElementsTranslationYAtPosition(px, position) },
+                            POSITION_TOP or POSITION_BOTTOM
+                        )
                     }
+                }
 
-                    launch {
-                        dreamViewModel.dreamOverlayTranslationX.collect { px ->
-                            ComplicationLayoutParams.iteratePositions(
-                                { position: Int ->
-                                    setElementsTranslationXAtPosition(px, position)
-                                },
-                                POSITION_TOP or POSITION_BOTTOM
-                            )
-                        }
+                launch {
+                    dreamViewModel.dreamOverlayTranslationX.collect { px ->
+                        ComplicationLayoutParams.iteratePositions(
+                            { position: Int -> setElementsTranslationXAtPosition(px, position) },
+                            POSITION_TOP or POSITION_BOTTOM
+                        )
                     }
+                }
 
-                    launch {
-                        dreamViewModel.dreamOverlayAlpha.collect { alpha ->
-                            ComplicationLayoutParams.iteratePositions(
-                                { position: Int ->
-                                    setElementsAlphaAtPosition(
-                                        alpha = alpha,
-                                        position = position,
-                                        fadingOut = true,
-                                    )
-                                },
-                                POSITION_TOP or POSITION_BOTTOM
-                            )
-                        }
+                launch {
+                    dreamViewModel.dreamOverlayAlpha.collect { alpha ->
+                        ComplicationLayoutParams.iteratePositions(
+                            { position: Int ->
+                                setElementsAlphaAtPosition(
+                                    alpha = alpha,
+                                    position = position,
+                                    fadingOut = true,
+                                )
+                            },
+                            POSITION_TOP or POSITION_BOTTOM
+                        )
                     }
+                }
 
-                    launch {
-                        dreamViewModel.transitionEnded.collect { _ ->
-                            mOverlayStateController.setExitAnimationsRunning(false)
-                        }
+                launch {
+                    dreamViewModel.transitionEnded.collect { _ ->
+                        mOverlayStateController.setExitAnimationsRunning(false)
                     }
                 }
             }
-    }
-
-    fun destroy() {
-        mLifecycleFlowHandle?.dispose()
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index bf6d266..76c7d23 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -59,7 +59,6 @@
 import com.android.systemui.util.ViewController;
 
 import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.DisposableHandle;
 import kotlinx.coroutines.flow.FlowKt;
 
 import java.util.Arrays;
@@ -186,8 +185,6 @@
                 }
             };
 
-    private DisposableHandle mFlowHandle;
-
     @Inject
     public DreamOverlayContainerViewController(
             DreamOverlayContainerView containerView,
@@ -255,17 +252,6 @@
     }
 
     @Override
-    public void destroy() {
-        mStateController.removeCallback(mDreamOverlayStateCallback);
-        mStatusBarViewController.destroy();
-        mComplicationHostViewController.destroy();
-        mDreamOverlayAnimationsController.destroy();
-        mLowLightTransitionCoordinator.setLowLightEnterListener(null);
-
-        super.destroy();
-    }
-
-    @Override
     protected void onViewAttached() {
         mWakingUpFromSwipe = false;
         mJitterStartTimeMillis = System.currentTimeMillis();
@@ -277,7 +263,7 @@
         emptyRegion.recycle();
 
         if (dreamHandlesBeingObscured()) {
-            mFlowHandle = collectFlow(
+            collectFlow(
                     mView,
                     FlowKt.distinctUntilChanged(combineFlows(
                             mKeyguardTransitionInteractor.isFinishedIn(
@@ -309,10 +295,6 @@
 
     @Override
     protected void onViewDetached() {
-        if (mFlowHandle != null) {
-            mFlowHandle.dispose();
-            mFlowHandle = null;
-        }
         mHandler.removeCallbacksAndMessages(null);
         mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
         mBouncerlessScrimController.removeCallback(mBouncerlessExpansionCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 0c1fb72..7a9537b 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -70,12 +70,8 @@
 import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
-import kotlinx.coroutines.Job;
-
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
-import java.util.concurrent.CancellationException;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
@@ -144,8 +140,6 @@
 
     private ComponentName mCurrentBlockedGestureDreamActivityComponent;
 
-    private final ArrayList<Job> mFlows = new ArrayList<>();
-
     /**
      * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch
      * handling, should be active. It will automatically be paused when the dream overlay is hidden
@@ -315,12 +309,12 @@
 
         mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED));
 
-        mFlows.add(collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(),
-                mIsCommunalAvailableCallback));
-        mFlows.add(collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
-                mCommunalVisibleConsumer));
-        mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
-                mBouncerShowingConsumer));
+        collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(),
+                mIsCommunalAvailableCallback);
+        collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
+                mCommunalVisibleConsumer);
+        collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
+                mBouncerShowingConsumer);
     }
 
     @NonNull
@@ -345,11 +339,6 @@
     public void onDestroy() {
         mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
 
-        for (Job job : mFlows) {
-            job.cancel(new CancellationException());
-        }
-        mFlows.clear();
-
         mExecutor.execute(() -> {
             setLifecycleStateLocked(Lifecycle.State.DESTROYED);
 
@@ -572,7 +561,6 @@
 
         if (mStarted && mWindow != null) {
             try {
-                mWindow.clearContentView();
                 mWindowManager.removeView(mWindow.getDecorView());
             } catch (IllegalArgumentException e) {
                 Log.e(TAG, "Error removing decor view when resetting overlay", e);
@@ -583,10 +571,7 @@
         mStateController.setLowLightActive(false);
         mStateController.setEntryAnimationsFinished(false);
 
-        if (mDreamOverlayContainerViewController != null) {
-            mDreamOverlayContainerViewController.destroy();
-            mDreamOverlayContainerViewController = null;
-        }
+        mDreamOverlayContainerViewController = null;
 
         if (mTouchMonitor != null) {
             mTouchMonitor.destroy();
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
index 5ba780f..ee7b6f5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -33,11 +33,7 @@
 import com.android.systemui.dreams.touch.dagger.CommunalTouchModule;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
-import kotlinx.coroutines.Job;
-
-import java.util.ArrayList;
 import java.util.Optional;
-import java.util.concurrent.CancellationException;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
@@ -53,8 +49,6 @@
     private final ConfigurationInteractor mConfigurationInteractor;
     private Boolean mIsEnabled = false;
 
-    private ArrayList<Job> mFlows = new ArrayList<>();
-
     private int mLayoutDirection = LayoutDirection.LTR;
 
     @VisibleForTesting
@@ -76,17 +70,17 @@
         mCommunalInteractor = communalInteractor;
         mConfigurationInteractor = configurationInteractor;
 
-        mFlows.add(collectFlow(
+        collectFlow(
                 mLifecycle,
                 mCommunalInteractor.isCommunalAvailable(),
                 mIsCommunalAvailableCallback
-        ));
+        );
 
-        mFlows.add(collectFlow(
+        collectFlow(
                 mLifecycle,
                 mConfigurationInteractor.getLayoutDirection(),
                 mLayoutDirectionCallback
-        ));
+        );
     }
 
     @Override
@@ -146,13 +140,4 @@
             }
         });
     }
-
-    @Override
-    public void onDestroy() {
-        for (Job job : mFlows) {
-            job.cancel(new CancellationException());
-        }
-        mFlows.clear();
-        TouchHandler.super.onDestroy();
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
index 104b076..32c4760 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
@@ -18,13 +18,45 @@
 
 import android.view.View
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.snapshots.StateFactoryMarker
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
 
 /** Base class for all System UI view-models. */
 abstract class SysUiViewModel : BaseActivatable() {
 
+    @StateFactoryMarker
+    fun <T> hydratedStateOf(
+        source: StateFlow<T>,
+    ): State<T> {
+        return hydratedStateOf(
+            initialValue = source.value,
+            source = source,
+        )
+    }
+
+    @StateFactoryMarker
+    fun <T> hydratedStateOf(
+        initialValue: T,
+        source: Flow<T>,
+    ): State<T> {
+        val mutableState = mutableStateOf(initialValue)
+        addChild(
+            object : BaseActivatable() {
+                override suspend fun onActivated(): Nothing {
+                    source.collect { mutableState.value = it }
+                    awaitCancellation()
+                }
+            }
+        )
+        return mutableState
+    }
+
     override suspend fun onActivated(): Nothing {
         awaitCancellation()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 388272f..0f82e02 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -73,8 +73,8 @@
 
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.policy.GestureNavigationSettingsObserver;
-import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.contextualeducation.GestureType;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
@@ -102,6 +102,8 @@
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.pip.Pip;
 
+import kotlinx.coroutines.Job;
+
 import java.io.PrintWriter;
 import java.util.ArrayDeque;
 import java.util.Date;
@@ -109,6 +111,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
@@ -158,7 +161,7 @@
     private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
         @Override
         public void onTaskStackChanged() {
-            updateRunningActivityGesturesBlocked();
+            updateTopActivity();
         }
         @Override
         public void onTaskCreated(int taskId, ComponentName componentName) {
@@ -222,6 +225,8 @@
     private final Provider<LightBarController> mLightBarControllerProvider;
 
     private final GestureInteractor mGestureInteractor;
+    private final ArraySet<ComponentName> mBlockedActivities = new ArraySet<>();
+    private Job mBlockedActivitiesJob = null;
 
     private final JavaAdapter mJavaAdapter;
 
@@ -450,9 +455,6 @@
         mJavaAdapter = javaAdapter;
         mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
 
-        mJavaAdapter.alwaysCollectFlow(mGestureInteractor.getGestureBlockedActivities(),
-                componentNames -> updateRunningActivityGesturesBlocked());
-
         ComponentName recentsComponentName = ComponentName.unflattenFromString(
                 context.getString(com.android.internal.R.string.config_recentsComponentName));
         if (recentsComponentName != null) {
@@ -568,12 +570,11 @@
         }
     }
 
-    private void updateRunningActivityGesturesBlocked() {
+    private void updateTopActivity() {
         if (edgebackGestureHandlerGetRunningTasksBackground()) {
-            mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set(
-                    isGestureBlockingActivityRunning()));
+            mBackgroundExecutor.execute(() -> updateTopActivityPackageName());
         } else {
-            mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning());
+            updateTopActivityPackageName();
         }
     }
 
@@ -678,6 +679,11 @@
                     Log.e(TAG, "Failed to unregister window manager callbacks", e);
                 }
 
+                if (mBlockedActivitiesJob != null) {
+                    mBlockedActivitiesJob.cancel(new CancellationException());
+                    mBlockedActivitiesJob = null;
+                }
+                mBlockedActivities.clear();
             } else {
                 mBackgroundExecutor.execute(mGestureNavigationSettingsObserver::register);
                 updateDisplaySize();
@@ -710,6 +716,12 @@
                 resetEdgeBackPlugin();
                 mPluginManager.addPluginListener(
                         this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false);
+
+                // Begin listening to changes in blocked activities list
+                mBlockedActivitiesJob = mJavaAdapter.alwaysCollectFlow(
+                        mGestureInteractor.getTopActivityBlocked(),
+                        blocked -> mGestureBlockingActivityRunning.set(blocked));
+
             }
             // Update the ML model resources.
             updateMLModelState();
@@ -1302,7 +1314,7 @@
         }
     }
 
-    private boolean isGestureBlockingActivityRunning() {
+    private void updateTopActivityPackageName() {
         ActivityManager.RunningTaskInfo runningTask =
                 ActivityManagerWrapper.getInstance().getRunningTask();
         ComponentName topActivity = runningTask == null ? null : runningTask.topActivity;
@@ -1311,8 +1323,6 @@
         } else {
             mPackageName = "_UNKNOWN";
         }
-
-        return topActivity != null && mGestureInteractor.areGesturesBlocked(topActivity);
     }
 
     public void setBackAnimation(BackAnimation backAnimation) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
index 6dc5939..6182878 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
@@ -17,17 +17,29 @@
 package com.android.systemui.navigationbar.gestural.domain
 
 import android.content.ComponentName
+import com.android.app.tracing.coroutines.flow.flowOn
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+import com.android.systemui.util.kotlin.combine
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 /**
  * {@link GestureInteractor} helps interact with gesture-related logic, including accessing the
@@ -37,7 +49,11 @@
 @Inject
 constructor(
     private val gestureRepository: GestureRepository,
-    @Application private val scope: CoroutineScope
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Background private val backgroundCoroutineContext: CoroutineContext,
+    @Application private val scope: CoroutineScope,
+    private val activityManagerWrapper: ActivityManagerWrapper,
+    private val taskStackChangeListeners: TaskStackChangeListeners,
 ) {
     enum class Scope {
         Local,
@@ -45,16 +61,38 @@
     }
 
     private val _localGestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(setOf())
-    /** A {@link StateFlow} for listening to changes in Activities where gestures are blocked */
-    val gestureBlockedActivities: StateFlow<Set<ComponentName>>
-        get() =
-            combine(
-                    gestureRepository.gestureBlockedActivities,
-                    _localGestureBlockedActivities.asStateFlow()
-                ) { global, local ->
-                    global + local
-                }
-                .stateIn(scope, SharingStarted.WhileSubscribed(), setOf())
+
+    private val _topActivity =
+        conflatedCallbackFlow {
+                val taskListener =
+                    object : TaskStackChangeListener {
+                        override fun onTaskStackChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                taskStackChangeListeners.registerTaskStackListener(taskListener)
+                awaitClose { taskStackChangeListeners.unregisterTaskStackListener(taskListener) }
+            }
+            .flowOn(mainDispatcher)
+            .emitOnStart()
+            .mapLatest { getTopActivity() }
+            .distinctUntilChanged()
+
+    private suspend fun getTopActivity(): ComponentName? =
+        withContext(backgroundCoroutineContext) {
+            val runningTask = activityManagerWrapper.runningTask
+            runningTask?.topActivity
+        }
+
+    val topActivityBlocked =
+        combine(
+            _topActivity,
+            gestureRepository.gestureBlockedActivities,
+            _localGestureBlockedActivities.asStateFlow()
+        ) { activity, global, local ->
+            activity != null && (global + local).contains(activity)
+        }
 
     /**
      * Adds an {@link Activity} to be blocked based on component when the topmost, focused {@link
@@ -92,12 +130,4 @@
             }
         }
     }
-
-    /**
-     * Checks whether the specified {@link Activity} {@link ComponentName} is being blocked from
-     * gestures.
-     */
-    fun areGesturesBlocked(activity: ComponentName): Boolean {
-        return gestureBlockedActivities.value.contains(activity)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS
index 408fc6d..32d37ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS
@@ -16,9 +16,9 @@
 per-file *Keyguard* = file:../keyguard/OWNERS
 per-file *Notification* = set noparent
 per-file *Notification* = file:notification/OWNERS
-per-file *Mode* = set noparent
+# Not setting noparent here, since *Mode* matches many other classes (e.g., *ViewModel*)
 per-file *Mode* = file:notification/OWNERS
 per-file *RemoteInput* = set noparent
 per-file *RemoteInput* = file:notification/OWNERS
 per-file *EmptyShadeView* = set noparent
-per-file *EmptyShadeView* = file:notification/OWNERS
\ No newline at end of file
+per-file *EmptyShadeView* = file:notification/OWNERS
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt
index 069ae93..28e3995 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt
@@ -44,4 +44,10 @@
 
     /** Snooze the currently pinned HUN. */
     fun snooze()
+
+    /** Unpin all currently pinned HUNs. */
+    fun unpinAll(userUnPinned: Boolean)
+
+    /** Release entries that were waiting for a shade expansion to complete. */
+    fun releaseAfterExpansion()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index 24b75d4..74ec7ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -148,6 +148,16 @@
     fun snooze() {
         headsUpRepository.snooze()
     }
+
+    /** Unpin all currently pinned HUNs. */
+    fun unpinAll(userUnPinned: Boolean) {
+        headsUpRepository.unpinAll(userUnPinned)
+    }
+
+    /** Notifies that the current scene transition is idle. */
+    fun onTransitionIdle() {
+        headsUpRepository.releaseAfterExpansion()
+    }
 }
 
 class HeadsUpRowInteractor(repository: HeadsUpRowRepository)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index e802076..0e4be8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1721,6 +1721,19 @@
         if (mTopHeadsUpRow == null) {
             return 0;
         }
+        ExpandableNotificationRow row = getTopHeadsUpRow();
+        return row.getPinnedHeadsUpHeight();
+    }
+
+    private int getTopHeadsUpIntrinsicHeight() {
+        if (mTopHeadsUpRow == null) {
+            return 0;
+        }
+        ExpandableNotificationRow row = getTopHeadsUpRow();
+        return row.getIntrinsicHeight();
+    }
+
+    private ExpandableNotificationRow getTopHeadsUpRow() {
         ExpandableNotificationRow row = mTopHeadsUpRow;
         if (row.isChildInGroup()) {
             final NotificationEntry groupSummary =
@@ -1729,7 +1742,7 @@
                 row = groupSummary.getRow();
             }
         }
-        return row.getPinnedHeadsUpHeight();
+        return row;
     }
 
     /**
@@ -2511,7 +2524,7 @@
 
     @Override
     public int getTopHeadsUpHeight() {
-        return getTopHeadsUpPinnedHeight();
+        return getTopHeadsUpIntrinsicHeight();
     }
 
     /**
@@ -5732,7 +5745,7 @@
         return mDisallowScrollingInThisMotion;
     }
 
-    boolean isBeingDragged() {
+    public boolean isBeingDragged() {
         return mIsBeingDragged;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index fa12bb9..693e8ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -2053,7 +2053,6 @@
                 hunWantsIt = mHeadsUpTouchHelper.onInterceptTouchEvent(ev);
                 if (hunWantsIt) {
                     mView.startDraggingOnHun();
-                    mHeadsUpManager.unpinAll(true);
                 }
             }
             boolean swipeWantsIt = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index a30b877..950b14d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
 import android.util.Log
+import com.android.app.tracing.coroutines.flow.filter
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.common.ui.view.onLayoutChanged
 import com.android.systemui.dagger.SysUISingleton
@@ -86,6 +87,7 @@
             }
             launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
             launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
+            launch { viewModel.shouldResetStackTop.filter { it }.collect { view.setStackTop(0f) } }
 
             launchAndDispose {
                 view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index a205179..6b95e98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -43,6 +43,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
 
 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
 class NotificationScrollViewModel
@@ -117,6 +118,14 @@
             .distinctUntilChanged()
             .dumpWhileCollecting("expandFraction")
 
+    val shouldResetStackTop: Flow<Boolean> =
+        sceneInteractor.transitionState
+            .mapNotNull { state ->
+                state is ObservableTransitionState.Idle && state.currentScene == Scenes.Gone
+            }
+            .distinctUntilChanged()
+            .dumpWhileCollecting("shouldResetStackTop")
+
     private operator fun SceneKey.contains(scene: SceneKey) =
         sceneInteractor.isSceneInFamily(scene, this)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 2fbb23e..ffa1de7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.lifecycle.SysUiViewModel
+import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
@@ -30,7 +32,11 @@
 import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
 
 /**
  * ViewModel used by the Notification placeholders inside the scene container to update the
@@ -40,7 +46,8 @@
 @AssistedInject
 constructor(
     private val interactor: NotificationStackAppearanceInteractor,
-    shadeInteractor: ShadeInteractor,
+    private val sceneInteractor: SceneInteractor,
+    private val shadeInteractor: ShadeInteractor,
     private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
     featureFlags: FeatureFlagsClassic,
     dumpManager: DumpManager,
@@ -58,6 +65,20 @@
     val isDebugLoggingEnabled: Boolean = SceneContainerFlag.isEnabled
 
     override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch {
+                shadeInteractor.isAnyExpanded
+                    .filter { it }
+                    .collect { headsUpNotificationInteractor.unpinAll(true) }
+            }
+
+            launch {
+                sceneInteractor.transitionState
+                    .map { state -> state is ObservableTransitionState.Idle }
+                    .filter { it }
+                    .collect { headsUpNotificationInteractor.onTransitionIdle() }
+            }
+        }
         activateFlowDumper()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
index 8d73983..dc15970 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
@@ -51,7 +51,9 @@
                             }
                             removed.forEach { key ->
                                 val row = obtainView(key)
-                                parentView.generateHeadsUpAnimation(row, /* isHeadsUp= */ false)
+                                if (!parentView.isBeingDragged()) {
+                                    parentView.generateHeadsUpAnimation(row, /* isHeadsUp= */ false)
+                                }
                                 row.markHeadsUpSeen()
                             }
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 544a8a5..720b257 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -35,6 +35,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -60,6 +61,11 @@
 import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.time.SystemClock;
 
+import kotlinx.coroutines.flow.Flow;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -70,11 +76,6 @@
 
 import javax.inject.Inject;
 
-import kotlinx.coroutines.flow.Flow;
-import kotlinx.coroutines.flow.MutableStateFlow;
-import kotlinx.coroutines.flow.StateFlow;
-import kotlinx.coroutines.flow.StateFlowKt;
-
 /** A implementation of HeadsUpManager for phone. */
 @SysUISingleton
 public class HeadsUpManagerPhone extends BaseHeadsUpManager implements
@@ -251,6 +252,12 @@
         return entry != null && mSystemClock.elapsedRealtime() < entry.mPostTime;
     }
 
+    @Override
+    public void releaseAfterExpansion() {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        onExpandingFinished();
+    }
+
     public void onExpandingFinished() {
         if (mReleaseOnExpandFinish) {
             releaseAllImmediately();
@@ -297,6 +304,11 @@
         }
     }
 
+    @Override
+    public void unpinAll(boolean userUnPinned) {
+        super.unpinAll(userUnPinned);
+    }
+
     /**
      * Notifies that a remote input textbox in notification gets active or inactive.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/DeviceBasedSatelliteTableLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/DeviceBasedSatelliteTableLog.kt
new file mode 100644
index 0000000..a40d510
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/DeviceBasedSatelliteTableLog.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.dagger
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class DeviceBasedSatelliteTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index a81bfa4..4850049 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -239,6 +239,13 @@
             return factory.create("VerboseDeviceBasedSatelliteInputLog", 200)
         }
 
+        @Provides
+        @SysUISingleton
+        @DeviceBasedSatelliteTableLog
+        fun provideDeviceBasedSatelliteTableLog(factory: TableLogBufferFactory): TableLogBuffer {
+            return factory.create("DeviceBasedSatelliteTableLog", 200)
+        }
+
         const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
             "FirstMobileSubShowingNetworkTypeIcon"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index cc4d568..26553e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -385,7 +385,15 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> =
-        mobileConnectionsRepo.deviceServiceState.map { it?.isEmergencyOnly ?: false }
+        mobileConnectionsRepo.deviceServiceState
+            .map { it?.isEmergencyOnly ?: false }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLogger,
+                columnPrefix = LOGGING_PREFIX,
+                columnName = "deviceEmergencyOnly",
+                initialValue = false,
+            )
 
     /** Vends out new [MobileIconInteractor] for a particular subId */
     override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
index 03f88c7..f1a444f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -21,7 +21,10 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
+import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteTableLog
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
@@ -33,6 +36,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -47,6 +51,7 @@
     wifiInteractor: WifiInteractor,
     @Application scope: CoroutineScope,
     @DeviceBasedSatelliteInputLog private val logBuffer: LogBuffer,
+    @DeviceBasedSatelliteTableLog private val tableLog: TableLogBuffer,
 ) {
     /** Must be observed by any UI showing Satellite iconography */
     val isSatelliteAllowed =
@@ -55,6 +60,13 @@
             } else {
                 flowOf(false)
             }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLog,
+                columnPrefix = "",
+                columnName = COL_ALLOWED,
+                initialValue = false,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     /** See [SatelliteConnectionState] for relevant states */
@@ -65,6 +77,12 @@
 
                 flowOf(SatelliteConnectionState.Off)
             }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLog,
+                columnPrefix = "",
+                initialValue = SatelliteConnectionState.Off,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), SatelliteConnectionState.Off)
 
     /** 0-4 description of the connection strength */
@@ -74,6 +92,13 @@
             } else {
                 flowOf(0)
             }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLog,
+                columnPrefix = "",
+                columnName = COL_LEVEL,
+                initialValue = 0,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
 
     val isSatelliteProvisioned = repo.isSatelliteProvisioned
@@ -82,19 +107,27 @@
         wifiInteractor.wifiNetwork.map { it is WifiNetworkModel.Active }
 
     private val allConnectionsOos =
-        iconsInteractor.icons.aggregateOver(
-            selector = { intr ->
-                combine(intr.isInService, intr.isEmergencyOnly, intr.isNonTerrestrial) {
-                    isInService,
-                    isEmergencyOnly,
-                    isNtn ->
-                    !isInService && !isEmergencyOnly && !isNtn
-                }
-            },
-            defaultValue = true, // no connections == everything is OOS
-        ) { isOosAndNotEmergencyAndNotSatellite ->
-            isOosAndNotEmergencyAndNotSatellite.all { it }
-        }
+        iconsInteractor.icons
+            .aggregateOver(
+                selector = { intr ->
+                    combine(intr.isInService, intr.isEmergencyOnly, intr.isNonTerrestrial) {
+                        isInService,
+                        isEmergencyOnly,
+                        isNtn ->
+                        !isInService && !isEmergencyOnly && !isNtn
+                    }
+                },
+                defaultValue = true, // no connections == everything is OOS
+            ) { isOosAndNotEmergencyAndNotSatellite ->
+                isOosAndNotEmergencyAndNotSatellite.all { it }
+            }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLog,
+                columnPrefix = "",
+                columnName = COL_ALL_OOS,
+                initialValue = true,
+            )
 
     /** When all connections are considered OOS, satellite connectivity is potentially valid */
     val areAllConnectionsOutOfService =
@@ -122,10 +155,24 @@
             } else {
                 flowOf(false)
             }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLog,
+                columnPrefix = "",
+                columnName = COL_FULL_OOS,
+                initialValue = true,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), true)
 
     companion object {
         const val TAG = "DeviceBasedSatelliteInteractor"
+
+        const val COL_LEVEL = "level"
+        const val COL_ALL_OOS = "allConnsOOS"
+        const val COL_ALLOWED = "allowed"
+        // Going to try to optimize for not using too much width on the table here. This information
+        // can be ascertained by checking for the device emergency only in the mobile logs as well
+        const val COL_FULL_OOS = "allOosAndNoEmer"
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
index bfe2941..905ed730 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
@@ -26,8 +26,10 @@
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
 
-enum class SatelliteConnectionState {
+enum class SatelliteConnectionState : Diffable<SatelliteConnectionState> {
     // State is unknown or undefined
     Unknown,
     // Radio is off
@@ -37,7 +39,15 @@
     // Radio is connected, aka satellite is available for use
     Connected;
 
+    override fun logDiffs(prevVal: SatelliteConnectionState, row: TableRowLogger) {
+        if (prevVal != this) {
+            row.logChange(COL_CONNECTION_STATE, name)
+        }
+    }
+
     companion object {
+        const val COL_CONNECTION_STATE = "connState"
+
         // TODO(b/316635648): validate these states. We don't need the level of granularity that
         //  SatelliteManager gives us.
         fun fromModemState(@SatelliteManager.SatelliteModemState modemState: Int) =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 48278d4..199b5b67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -22,9 +22,12 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
+import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteTableLog
 import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
 import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
@@ -71,22 +74,34 @@
     @Application scope: CoroutineScope,
     airplaneModeRepository: AirplaneModeRepository,
     @DeviceBasedSatelliteInputLog logBuffer: LogBuffer,
+    @DeviceBasedSatelliteTableLog tableLog: TableLogBuffer,
 ) : DeviceBasedSatelliteViewModel {
     private val shouldShowIcon: Flow<Boolean> =
-        interactor.areAllConnectionsOutOfService.flatMapLatest { allOos ->
-            if (!allOos) {
-                flowOf(false)
-            } else {
-                combine(
-                    interactor.isSatelliteAllowed,
-                    interactor.isSatelliteProvisioned,
-                    interactor.isWifiActive,
-                    airplaneModeRepository.isAirplaneMode
-                ) { isSatelliteAllowed, isSatelliteProvisioned, isWifiActive, isAirplaneMode ->
-                    isSatelliteAllowed && isSatelliteProvisioned && !isWifiActive && !isAirplaneMode
+        interactor.areAllConnectionsOutOfService
+            .flatMapLatest { allOos ->
+                if (!allOos) {
+                    flowOf(false)
+                } else {
+                    combine(
+                        interactor.isSatelliteAllowed,
+                        interactor.isSatelliteProvisioned,
+                        interactor.isWifiActive,
+                        airplaneModeRepository.isAirplaneMode
+                    ) { isSatelliteAllowed, isSatelliteProvisioned, isWifiActive, isAirplaneMode ->
+                        isSatelliteAllowed &&
+                            isSatelliteProvisioned &&
+                            !isWifiActive &&
+                            !isAirplaneMode
+                    }
                 }
             }
-        }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLog,
+                columnPrefix = "vm",
+                columnName = COL_VISIBLE_CONDITION,
+                initialValue = false,
+            )
 
     // This adds a 10 seconds delay before showing the icon
     private val shouldActuallyShowIcon: StateFlow<Boolean> =
@@ -106,6 +121,13 @@
                     flowOf(false)
                 }
             }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLog,
+                columnPrefix = "vm",
+                columnName = COL_VISIBLE,
+                initialValue = false,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val icon: StateFlow<Icon?> =
@@ -163,5 +185,8 @@
     companion object {
         private const val TAG = "DeviceBasedSatelliteViewModel"
         private val DELAY_DURATION = 10.seconds
+
+        const val COL_VISIBLE_CONDITION = "visCondition"
+        const val COL_VISIBLE = "visible"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
index 3fffd9f..8b50f84 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
@@ -33,6 +33,9 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import com.android.systemui.common.ui.compose.Icon
@@ -58,7 +61,9 @@
                             onClick = viewModel.onClick,
                             onLongClick = viewModel.onLongClick
                         )
-                        .padding(20.dp),
+                        .padding(20.dp)
+                        .semantics(mergeDescendants = true) {}
+                        .clearAndSetSemantics { contentDescription = viewModel.contentDescription },
                 verticalAlignment = Alignment.CenterVertically,
                 horizontalArrangement =
                     Arrangement.spacedBy(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
index 7c1cb6a..921d79b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
@@ -28,6 +28,7 @@
     val icon: Icon,
     val text: String,
     val subtext: String,
+    val contentDescription: String,
     val enabled: Boolean,
     val onClick: () -> Unit,
     val onLongClick: () -> Unit,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
index 5772099..38bade0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -89,8 +89,9 @@
                     ModeTileViewModel(
                         id = mode.id,
                         icon = zenModeInteractor.getModeIcon(mode, context),
-                        text = mode.rule.name,
+                        text = mode.name,
                         subtext = getTileSubtext(mode),
+                        contentDescription = getTileContentDescription(mode),
                         enabled = mode.isActive,
                         onClick = {
                             if (!mode.rule.isEnabled) {
@@ -135,9 +136,35 @@
             return context.resources.getString(R.string.zen_mode_no_manual_invocation)
         }
 
-        val on = context.resources.getString(R.string.zen_mode_on)
-        val off = context.resources.getString(R.string.zen_mode_off)
-        return mode.getDynamicDescription(context) ?: if (mode.isActive) on else off
+        val modeSubtext = mode.getDynamicDescription(context)
+        return if (mode.isActive) {
+            if (modeSubtext != null) {
+                context.getString(R.string.zen_mode_on_with_details, modeSubtext)
+            } else {
+                context.getString(R.string.zen_mode_on)
+            }
+        } else {
+            modeSubtext ?: context.getString(R.string.zen_mode_off)
+        }
+    }
+
+    private fun getTileContentDescription(mode: ZenMode): String {
+        return buildList {
+                add(mode.name)
+                if (!mode.rule.isEnabled) {
+                    add(context.getString(R.string.zen_mode_set_up))
+                } else if (!mode.rule.isManualInvocationAllowed && !mode.isActive) {
+                    add(context.getString(R.string.zen_mode_no_manual_invocation))
+                } else {
+                    add(
+                        context.getString(
+                            if (mode.isActive) R.string.zen_mode_on else R.string.zen_mode_off
+                        )
+                    )
+                    mode.getDynamicDescription(context)?.let { add(it) }
+                }
+            }
+            .joinToString(separator = "\n")
     }
 
     private fun makeZenModeDialog(): Dialog {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 055671c..28ac2c0 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -28,7 +28,6 @@
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -63,9 +62,7 @@
 /**
  * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to
  * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners
- * during onViewAttached() and removing during onViewRemoved().
- *
- * @return a disposable handle in order to cancel the flow in the future.
+ * during onViewAttached() and removing during onViewRemoved()
  */
 @JvmOverloads
 fun <T> collectFlow(
@@ -74,8 +71,8 @@
     consumer: Consumer<T>,
     coroutineContext: CoroutineContext = EmptyCoroutineContext,
     state: Lifecycle.State = Lifecycle.State.CREATED,
-): DisposableHandle {
-    return view.repeatWhenAttached(coroutineContext) {
+) {
+    view.repeatWhenAttached(coroutineContext) {
         repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 2468449..eb91518 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -695,11 +695,10 @@
             addRow(AudioManager.STREAM_MUSIC,
                     R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true);
             if (!AudioSystem.isSingleVolume(mContext)) {
-
                 addRow(AudioManager.STREAM_RING, R.drawable.ic_ring_volume,
                         R.drawable.ic_ring_volume_off, true, false);
-
-
+                addRow(AudioManager.STREAM_NOTIFICATION, R.drawable.ic_volume_ringer,
+                        R.drawable.ic_volume_off, true, false);
                 addRow(STREAM_ALARM,
                         R.drawable.ic_alarm, R.drawable.ic_volume_alarm_mute, true, false);
                 addRow(AudioManager.STREAM_VOICE_CALL,
@@ -1994,7 +1993,7 @@
                                             : R.drawable.ic_volume_media_bt;
             }
         } else if (isStreamMuted(ss)) {
-            iconRes = ss.muted ? R.drawable.ic_volume_media_off : row.iconMuteRes;
+            iconRes = (ss.muted && isTv()) ? R.drawable.ic_volume_media_off : row.iconMuteRes;
         } else {
             iconRes = mShowLowMediaVolumeIcon && ss.level * 2 < (ss.levelMax + ss.levelMin)
                       ? R.drawable.ic_volume_media_low : row.iconRes;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
index a18d272..5600b87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
@@ -711,16 +711,6 @@
     }
 
     @Test
-    public void testDestroy_cleansUpHandler() {
-        final TouchHandler touchHandler = createTouchHandler();
-
-        final Environment environment = new Environment(Stream.of(touchHandler)
-                .collect(Collectors.toCollection(HashSet::new)), mKosmos);
-        environment.destroyMonitor();
-        verify(touchHandler).onDestroy();
-    }
-
-    @Test
     public void testLastSessionPop_createsNewInputSession() {
         final TouchHandler touchHandler = createTouchHandler();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt
new file mode 100644
index 0000000..432f7af
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputdevice.tutorial.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
+import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository
+import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType
+import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.touchpad.data.repository.FakeTouchpadRepository
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.hours
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TutorialSchedulerInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: TutorialSchedulerInteractor
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+    private lateinit var dataStoreScope: CoroutineScope
+    private val keyboardRepository = FakeKeyboardRepository()
+    private val touchpadRepository = FakeTouchpadRepository()
+    private lateinit var schedulerRepository: TutorialSchedulerRepository
+
+    @Before
+    fun setup() {
+        dataStoreScope = CoroutineScope(Dispatchers.Unconfined)
+        schedulerRepository =
+            TutorialSchedulerRepository(
+                context,
+                dataStoreScope,
+                dataStoreName = "TutorialSchedulerInteractorTest"
+            )
+        underTest =
+            TutorialSchedulerInteractor(
+                testScope.backgroundScope,
+                keyboardRepository,
+                touchpadRepository,
+                schedulerRepository
+            )
+        underTest.start()
+    }
+
+    @After
+    fun clear() {
+        runBlocking { schedulerRepository.clearDataStore() }
+        dataStoreScope.cancel()
+    }
+
+    @Test
+    fun connectKeyboard_delayElapse_launchForKeyboard() =
+        testScope.runTest {
+            keyboardRepository.setIsAnyKeyboardConnected(true)
+            advanceTimeBy(LAUNCH_DELAY)
+            assertLaunch(TutorialType.KEYBOARD)
+        }
+
+    @Test
+    fun connectBothDevices_delayElapse_launchForBoth() =
+        testScope.runTest {
+            keyboardRepository.setIsAnyKeyboardConnected(true)
+            touchpadRepository.setIsAnyTouchpadConnected(true)
+            advanceTimeBy(LAUNCH_DELAY)
+            assertLaunch(TutorialType.BOTH)
+        }
+
+    @Test
+    fun connectBothDevice_delayNotElapse_launchNothing() =
+        testScope.runTest {
+            keyboardRepository.setIsAnyKeyboardConnected(true)
+            touchpadRepository.setIsAnyTouchpadConnected(true)
+            advanceTimeBy(A_SHORT_PERIOD_OF_TIME)
+            assertLaunch(TutorialType.NONE)
+        }
+
+    @Test
+    fun nothingConnect_delayElapse_launchNothing() =
+        testScope.runTest {
+            keyboardRepository.setIsAnyKeyboardConnected(false)
+            touchpadRepository.setIsAnyTouchpadConnected(false)
+            advanceTimeBy(LAUNCH_DELAY)
+            assertLaunch(TutorialType.NONE)
+        }
+
+    @Test
+    fun connectKeyboard_thenTouchpad_delayElapse_launchForBoth() =
+        testScope.runTest {
+            keyboardRepository.setIsAnyKeyboardConnected(true)
+            advanceTimeBy(A_SHORT_PERIOD_OF_TIME)
+            touchpadRepository.setIsAnyTouchpadConnected(true)
+            advanceTimeBy(REMAINING_TIME)
+            assertLaunch(TutorialType.BOTH)
+        }
+
+    @Test
+    fun connectKeyboard_thenTouchpad_removeKeyboard_delayElapse_launchNothing() =
+        testScope.runTest {
+            keyboardRepository.setIsAnyKeyboardConnected(true)
+            advanceTimeBy(A_SHORT_PERIOD_OF_TIME)
+            touchpadRepository.setIsAnyTouchpadConnected(true)
+            keyboardRepository.setIsAnyKeyboardConnected(false)
+            advanceTimeBy(REMAINING_TIME)
+            assertLaunch(TutorialType.NONE)
+        }
+
+    // TODO: likely to be changed after we update TutorialSchedulerInteractor.launchTutorial
+    private suspend fun assertLaunch(tutorialType: TutorialType) {
+        when (tutorialType) {
+            TutorialType.KEYBOARD -> {
+                assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isTrue()
+                assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isFalse()
+            }
+            TutorialType.TOUCHPAD -> {
+                assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isFalse()
+                assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isTrue()
+            }
+            TutorialType.BOTH -> {
+                assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isTrue()
+                assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isTrue()
+            }
+            TutorialType.NONE -> {
+                assertThat(schedulerRepository.isLaunched(DeviceType.KEYBOARD)).isFalse()
+                assertThat(schedulerRepository.isLaunched(DeviceType.TOUCHPAD)).isFalse()
+            }
+        }
+    }
+
+    companion object {
+        private val LAUNCH_DELAY = 72.hours
+        private val A_SHORT_PERIOD_OF_TIME = 2.hours
+        private val REMAINING_TIME = 70.hours
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
index cd0390e..dbb77d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -70,6 +70,7 @@
                 wifiInteractor,
                 testScope.backgroundScope,
                 FakeLogBuffer.Factory.create(),
+                mock(),
             )
     }
 
@@ -113,6 +114,7 @@
                     wifiInteractor,
                     testScope.backgroundScope,
                     FakeLogBuffer.Factory.create(),
+                    mock(),
                 )
 
             val latest by collectLastValue(underTest.isSatelliteAllowed)
@@ -161,6 +163,7 @@
                     wifiInteractor,
                     testScope.backgroundScope,
                     FakeLogBuffer.Factory.create(),
+                    mock(),
                 )
 
             val latest by collectLastValue(underTest.connectionState)
@@ -217,6 +220,7 @@
                     wifiInteractor,
                     testScope.backgroundScope,
                     FakeLogBuffer.Factory.create(),
+                    mock(),
                 )
 
             val latest by collectLastValue(underTest.signalStrength)
@@ -535,6 +539,7 @@
                     wifiInteractor,
                     testScope.backgroundScope,
                     FakeLogBuffer.Factory.create(),
+                    mock(),
                 )
 
             val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 64b07fc..c3cc33f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
-import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
 import kotlin.time.Duration.Companion.seconds
@@ -44,6 +43,7 @@
 import org.junit.Before
 import org.junit.runner.RunWith
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -73,6 +73,7 @@
                 wifiInteractor,
                 testScope.backgroundScope,
                 FakeLogBuffer.Factory.create(),
+                mock(),
             )
 
         underTest =
@@ -82,6 +83,7 @@
                 testScope.backgroundScope,
                 airplaneModeRepository,
                 FakeLogBuffer.Factory.create(),
+                mock(),
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 9dd3e53..6e39365 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -102,6 +102,7 @@
 import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.logging.UiEventLogger;
+import com.android.internal.protolog.ProtoLog;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.launcher3.icons.BubbleIconFactory;
 import com.android.systemui.SysuiTestCase;
@@ -168,7 +169,6 @@
 import com.android.wm.shell.bubbles.BubbleDataRepository;
 import com.android.wm.shell.bubbles.BubbleEducationController;
 import com.android.wm.shell.bubbles.BubbleEntry;
-import com.android.wm.shell.bubbles.BubbleExpandedViewManager;
 import com.android.wm.shell.bubbles.BubbleLogger;
 import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubbleStackView;
@@ -381,6 +381,9 @@
 
     @Before
     public void setUp() throws Exception {
+        // Make sure ProtoLog is initialized before any logging occurs.
+        ProtoLog.init();
+
         MockitoAnnotations.initMocks(this);
         PhysicsAnimatorTestUtils.prepareForTest();
 
@@ -1404,7 +1407,6 @@
                 .thenReturn(userContext);
 
         BubbleViewInfoTask.BubbleViewInfo info = BubbleViewInfoTask.BubbleViewInfo.populate(context,
-                BubbleExpandedViewManager.fromBubbleController(mBubbleController),
                 () -> new BubbleTaskView(mock(TaskView.class), mock(Executor.class)),
                 mPositioner,
                 mBubbleController.getStackView(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
index 658aaa6..1d2439c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
@@ -16,12 +16,23 @@
 
 package com.android.systemui.keyguard.gesture.domain
 
-import com.android.systemui.keyguard.gesture.data.gestureRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigationbar.gestural.data.gestureRepository
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+import com.android.systemui.shared.system.activityManagerWrapper
+import com.android.systemui.shared.system.taskStackChangeListeners
 
 val Kosmos.gestureInteractor: GestureInteractor by
     Kosmos.Fixture {
-        GestureInteractor(gestureRepository = gestureRepository, scope = applicationCoroutineScope)
+        GestureInteractor(
+            gestureRepository = gestureRepository,
+            mainDispatcher = testDispatcher,
+            backgroundCoroutineContext = backgroundCoroutineContext,
+            scope = applicationCoroutineScope,
+            activityManagerWrapper = activityManagerWrapper,
+            taskStackChangeListeners = taskStackChangeListeners
+        )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
index c0bb9a6..90cd8c7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
@@ -16,15 +16,27 @@
 
 package com.android.systemui.lifecycle
 
+import androidx.compose.runtime.getValue
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOf
 
 class FakeSysUiViewModel(
     private val onActivation: () -> Unit = {},
     private val onDeactivation: () -> Unit = {},
+    private val upstreamFlow: Flow<Boolean> = flowOf(true),
+    private val upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(),
 ) : SysUiViewModel() {
+
     var activationCount = 0
     var cancellationCount = 0
 
+    val stateBackedByFlow: Boolean by hydratedStateOf(initialValue = true, source = upstreamFlow)
+    val stateBackedByStateFlow: Boolean by hydratedStateOf(source = upstreamStateFlow)
+
     override suspend fun onActivated(): Nothing {
         activationCount++
         onActivation()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
similarity index 94%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
index 9bd346e..55ce43a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.gesture.data
+package com.android.systemui.navigationbar.gestural.data
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testDispatcher
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
index 7e8f1a9..1fa6236 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
@@ -44,6 +44,14 @@
         // do nothing
     }
 
+    override fun unpinAll(userUnPinned: Boolean) {
+        // do nothing
+    }
+
+    override fun releaseAfterExpansion() {
+        // do nothing
+    }
+
     fun setNotifications(notifications: List<HeadsUpRowRepository>) {
         this.orderedHeadsUpRows.value = notifications.toList()
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
index 3247525..634354b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
@@ -27,6 +28,7 @@
 val Kosmos.notificationsPlaceholderViewModel by Fixture {
     NotificationsPlaceholderViewModel(
         interactor = notificationStackAppearanceInteractor,
+        sceneInteractor = sceneInteractor,
         shadeInteractor = shadeInteractor,
         headsUpNotificationInteractor = headsUpNotificationInteractor,
         featureFlags = featureFlagsClassic,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/data/repository/FakeTouchpadRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/data/repository/FakeTouchpadRepository.kt
new file mode 100644
index 0000000..1ec6bbf
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/data/repository/FakeTouchpadRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeTouchpadRepository : TouchpadRepository {
+
+    private val _isAnyTouchpadConnected = MutableStateFlow(false)
+    override val isAnyTouchpadConnected: Flow<Boolean> = _isAnyTouchpadConnected
+
+    fun setIsAnyTouchpadConnected(connected: Boolean) {
+        _isAnyTouchpadConnected.value = connected
+    }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
index f30e770..954651d 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
@@ -18,7 +18,6 @@
 
 import static android.app.appfunctions.flags.Flags.enableAppFunctionManager;
 
-import android.app.appfunctions.IAppFunctionManager;
 import android.content.Context;
 
 import com.android.server.SystemService;
@@ -27,19 +26,17 @@
  * Service that manages app functions.
  */
 public class AppFunctionManagerService extends SystemService {
+    private final AppFunctionManagerServiceImpl mServiceImpl;
 
     public AppFunctionManagerService(Context context) {
         super(context);
+        mServiceImpl = new AppFunctionManagerServiceImpl(context);
     }
 
     @Override
     public void onStart() {
         if (enableAppFunctionManager()) {
-            publishBinderService(Context.APP_FUNCTION_SERVICE, new AppFunctionManagerStub());
+            publishBinderService(Context.APP_FUNCTION_SERVICE, mServiceImpl);
         }
     }
-
-    private static class AppFunctionManagerStub extends IAppFunctionManager.Stub {
-
-    }
 }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
new file mode 100644
index 0000000..e2167a8
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
+import android.app.appfunctions.ExecuteAppFunctionResponse;
+import android.app.appfunctions.IAppFunctionManager;
+import android.app.appfunctions.IAppFunctionService;
+import android.app.appfunctions.IExecuteAppFunctionCallback;
+import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
+import android.app.appfunctions.ServiceCallHelper;
+import android.app.appfunctions.ServiceCallHelper.RunServiceCallCallback;
+import android.app.appfunctions.ServiceCallHelper.ServiceUsageCompleteListener;
+import android.app.appfunctions.ServiceCallHelperImpl;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implementation of the AppFunctionManagerService.
+ */
+public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
+    private static final String TAG = AppFunctionManagerServiceImpl.class.getSimpleName();
+    private final ServiceCallHelper<IAppFunctionService> mExternalServiceCallHelper;
+    private final CallerValidator mCallerValidator;
+    private final ServiceHelper mInternalServiceHelper;
+
+    public AppFunctionManagerServiceImpl(@NonNull Context context) {
+        this(new ServiceCallHelperImpl<>(
+                        context,
+                        IAppFunctionService.Stub::asInterface, new ThreadPoolExecutor(
+                        /*corePoolSize=*/ Runtime.getRuntime().availableProcessors(),
+                        /*maxConcurrency=*/ Runtime.getRuntime().availableProcessors(),
+                        /*keepAliveTime=*/ 0L,
+                        /*unit=*/ TimeUnit.SECONDS,
+                        /*workQueue=*/ new LinkedBlockingQueue<>())),
+                new CallerValidatorImpl(context),
+                new ServiceHelperImpl(context));
+    }
+
+    @VisibleForTesting
+    AppFunctionManagerServiceImpl(ServiceCallHelper<IAppFunctionService> serviceCallHelper,
+                                  CallerValidator apiValidator,
+                                  ServiceHelper appFunctionInternalServiceHelper) {
+        mExternalServiceCallHelper = Objects.requireNonNull(serviceCallHelper);
+        mCallerValidator = Objects.requireNonNull(apiValidator);
+        mInternalServiceHelper =
+                Objects.requireNonNull(appFunctionInternalServiceHelper);
+    }
+
+    @Override
+    public void executeAppFunction(
+            @NonNull ExecuteAppFunctionAidlRequest requestInternal,
+            @NonNull IExecuteAppFunctionCallback executeAppFunctionCallback) {
+        Objects.requireNonNull(requestInternal);
+        Objects.requireNonNull(executeAppFunctionCallback);
+
+        final SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback =
+                new SafeOneTimeExecuteAppFunctionCallback(executeAppFunctionCallback);
+
+        String validatedCallingPackage = mCallerValidator
+                .validateCallingPackage(requestInternal.getCallingPackage());
+        UserHandle targetUser = mCallerValidator.verifyTargetUserHandle(
+                requestInternal.getUserHandle(), validatedCallingPackage);
+
+        // TODO(b/354956319): Add and honor the new enterprise policies.
+        if (mCallerValidator.isUserOrganizationManaged(targetUser)) {
+            safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse.Builder(
+                    ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                    "Cannot run on a device with a device owner or from the managed profile."
+            ).build());
+            return;
+        }
+
+        String targetPackageName = requestInternal.getClientRequest().getTargetPackageName();
+        if (TextUtils.isEmpty(targetPackageName)) {
+            safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse.Builder(
+                    ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT,
+                    "Target package name cannot be empty."
+            ).build());
+            return;
+        }
+
+        if (!mCallerValidator.verifyCallerCanExecuteAppFunction(
+                validatedCallingPackage, targetPackageName)) {
+            throw new SecurityException("Caller does not have permission to execute the app "
+                    + "function.");
+        }
+
+        Intent serviceIntent = mInternalServiceHelper.resolveAppFunctionService(
+                targetPackageName,
+                targetUser);
+        if (serviceIntent == null) {
+            safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse.Builder(
+                    ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                    "Cannot find the target service."
+            ).build());
+            return;
+        }
+
+        // TODO(b/357551503): Offload call to async executor.
+        bindAppFunctionServiceUnchecked(requestInternal, serviceIntent, targetUser,
+                safeExecuteAppFunctionCallback,
+                /*bindFlags=*/ Context.BIND_AUTO_CREATE,
+                // TODO(b/357551503): Make timeout configurable.
+                /*timeoutInMillis=*/ 30_000L);
+    }
+
+    private void bindAppFunctionServiceUnchecked(
+            @NonNull ExecuteAppFunctionAidlRequest requestInternal,
+            @NonNull Intent serviceIntent, @NonNull UserHandle targetUser,
+            @NonNull SafeOneTimeExecuteAppFunctionCallback
+                    safeExecuteAppFunctionCallback,
+            int bindFlags, long timeoutInMillis) {
+        boolean bindServiceResult = mExternalServiceCallHelper.runServiceCall(
+                serviceIntent,
+                bindFlags,
+                timeoutInMillis,
+                targetUser,
+                /*timeOutCallback=*/ new RunServiceCallCallback<IAppFunctionService>() {
+                    @Override
+                    public void onServiceConnected(@NonNull IAppFunctionService service,
+                                                   @NonNull ServiceUsageCompleteListener
+                                                           serviceUsageCompleteListener) {
+                        try {
+                            service.executeAppFunction(
+                                    requestInternal.getClientRequest(),
+                                    new IExecuteAppFunctionCallback.Stub() {
+                                        @Override
+                                        public void onResult(ExecuteAppFunctionResponse response) {
+                                            safeExecuteAppFunctionCallback.onResult(response);
+                                            serviceUsageCompleteListener.onCompleted();
+                                        }
+                                    }
+                            );
+                        } catch (Exception e) {
+                            safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse
+                                    .Builder(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                                    e.getMessage()).build());
+                            serviceUsageCompleteListener.onCompleted();
+                        }
+                    }
+
+                    @Override
+                    public void onFailedToConnect() {
+                        Slog.e(TAG, "Failed to connect to service");
+                        safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse
+                                .Builder(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                                "Failed to connect to AppFunctionService").build());
+                    }
+
+                    @Override
+                    public void onTimedOut() {
+                        Slog.e(TAG, "Timed out");
+                        safeExecuteAppFunctionCallback.onResult(
+                                new ExecuteAppFunctionResponse.Builder(
+                                        ExecuteAppFunctionResponse.RESULT_TIMED_OUT,
+                                        "Binding to AppFunctionService timed out."
+                                ).build());
+                    }
+                }
+        );
+
+        if (!bindServiceResult) {
+            Slog.e(TAG, "Failed to bind to the AppFunctionService");
+            safeExecuteAppFunctionCallback.onResult(new ExecuteAppFunctionResponse.Builder(
+                    ExecuteAppFunctionResponse.RESULT_TIMED_OUT,
+                    "Failed to bind the AppFunctionService."
+            ).build());
+        }
+    }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
new file mode 100644
index 0000000..9bd633f
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.os.UserHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+
+/**
+ * Interface for validating that the caller has the correct privilege to call an AppFunctionManager
+ * API.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public interface CallerValidator {
+    // TODO(b/357551503): Should we verify NOT instant app?
+    // TODO(b/357551503): Verify that user have been unlocked.
+
+    /**
+     * This method is used to validate that the calling package reported in the request is the
+     * same as the binder calling identity.
+     *
+     * @param claimedCallingPackage The package name of the caller.
+     * @return The package name of the caller.
+     * @throws SecurityException if the package name and uid don't match.
+     */
+    String validateCallingPackage(@NonNull String claimedCallingPackage);
+
+    /**
+     * Validates that the caller can invoke an AppFunctionManager API in the provided
+     * target user space.
+     *
+     * @param targetUserHandle      The user which the caller is requesting to execute as.
+     * @param claimedCallingPackage The package name of the caller.
+     * @return The user handle that the call should run as. Will always be a concrete user.
+     * @throws IllegalArgumentException if the target user is a special user.
+     * @throws SecurityException        if caller trying to interact across users without {@link
+     *                                  Manifest.permission#INTERACT_ACROSS_USERS_FULL}
+     */
+    UserHandle verifyTargetUserHandle(@NonNull UserHandle targetUserHandle,
+                                      @NonNull String claimedCallingPackage);
+
+    /**
+     * Validates that the caller can execute the specified app function.
+     * <p>
+     * The caller can execute if the app function's package name is the same as the caller's package
+     * or the caller has either {@link Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or
+     * {@link Manifest.permission.EXECUTE_APP_FUNCTIONS} granted. In some cases, app functions
+     * can still opt-out of caller having {@link Manifest.permission.EXECUTE_APP_FUNCTIONS}.
+     *
+     * @param callerPackageName     The calling package (as previously validated).
+     * @param targetPackageName     The package that owns the app function to execute.
+     * @return Whether the caller can execute the specified app function.
+     */
+    boolean verifyCallerCanExecuteAppFunction(
+            @NonNull String callerPackageName, @NonNull String targetPackageName);
+
+    /**
+     * Checks if the user is organization managed.
+     *
+     * @param targetUser The user which the caller is requesting to execute as.
+     * @return Whether the user is organization managed.
+     */
+    boolean isUserOrganizationManaged(@NonNull UserHandle targetUser);
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
new file mode 100644
index 0000000..7cd660d
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.Manifest;
+import android.annotation.BinderThread;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import java.util.Objects;
+
+/* Validates that caller has the correct privilege to call an AppFunctionManager Api. */
+class CallerValidatorImpl implements CallerValidator {
+    private final Context mContext;
+
+
+    CallerValidatorImpl(@NonNull Context context) {
+        mContext = Objects.requireNonNull(context);
+    }
+
+    @Override
+    @NonNull
+    @BinderThread
+    public String validateCallingPackage(@NonNull String claimedCallingPackage) {
+        int callingUid = Binder.getCallingUid();
+        final long callingIdentityToken = Binder.clearCallingIdentity();
+        try {
+            validateCallingPackageInternal(callingUid, claimedCallingPackage);
+            return claimedCallingPackage;
+        } finally {
+            Binder.restoreCallingIdentity(callingIdentityToken);
+        }
+    }
+
+    @Override
+    @NonNull
+    @BinderThread
+    public UserHandle verifyTargetUserHandle(@NonNull UserHandle targetUserHandle,
+                                             @NonNull String claimedCallingPackage) {
+        int callingPid = Binder.getCallingPid();
+        int callingUid = Binder.getCallingUid();
+        final long callingIdentityToken = Binder.clearCallingIdentity();
+        try {
+            return handleIncomingUser(claimedCallingPackage, targetUserHandle,
+                    callingPid, callingUid);
+        } finally {
+            Binder.restoreCallingIdentity(callingIdentityToken);
+        }
+    }
+
+    @Override
+    @BinderThread
+    @RequiresPermission(anyOf = {Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,
+            Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)
+    // TODO(b/360864791): Add and honor apps that opt-out from EXECUTE_APP_FUNCTIONS caller.
+    public boolean verifyCallerCanExecuteAppFunction(
+            @NonNull String callerPackageName, @NonNull String targetPackageName) {
+        int pid = Binder.getCallingPid();
+        int uid = Binder.getCallingUid();
+        boolean hasExecutionPermission = mContext.checkPermission(
+                Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, pid, uid)
+                == PackageManager.PERMISSION_GRANTED;
+        boolean hasTrustedExecutionPermission = mContext.checkPermission(
+                Manifest.permission.EXECUTE_APP_FUNCTIONS, pid, uid)
+                == PackageManager.PERMISSION_GRANTED;
+        boolean isSamePackage = callerPackageName.equals(targetPackageName);
+        return hasExecutionPermission || hasTrustedExecutionPermission || isSamePackage;
+    }
+
+    @Override
+    @BinderThread
+    public boolean isUserOrganizationManaged(@NonNull UserHandle targetUser) {
+        final long callingIdentityToken = Binder.clearCallingIdentity();
+        try {
+            if (Objects.requireNonNull(mContext.getSystemService(DevicePolicyManager.class))
+                    .isDeviceManaged()) {
+                return true;
+            }
+            return Objects.requireNonNull(mContext.getSystemService(UserManager.class))
+                    .isManagedProfile(targetUser.getIdentifier());
+        } finally {
+            Binder.restoreCallingIdentity(callingIdentityToken);
+        }
+    }
+
+    /**
+     * Helper for dealing with incoming user arguments to system service calls.
+     *
+     * <p>Takes care of checking permissions and if the target is special user, this method will
+     * simply throw.
+     *
+     * @param callingPackageName The package name of the caller.
+     * @param targetUserHandle   The user which the caller is requesting to execute as.
+     * @param callingPid         The actual pid of the caller as determined by Binder.
+     * @param callingUid         The actual uid of the caller as determined by Binder.
+     * @return the user handle that the call should run as. Will always be a concrete user.
+     * @throws IllegalArgumentException if the target user is a special user.
+     * @throws SecurityException        if caller trying to interact across user without {@link
+     *                                  Manifest.permission#INTERACT_ACROSS_USERS_FULL}
+     */
+    @NonNull
+    private UserHandle handleIncomingUser(
+            @NonNull String callingPackageName,
+            @NonNull UserHandle targetUserHandle,
+            int callingPid,
+            int callingUid) {
+        UserHandle callingUserHandle = UserHandle.getUserHandleForUid(callingUid);
+        if (callingUserHandle.equals(targetUserHandle)) {
+            return targetUserHandle;
+        }
+
+        // Duplicates UserController#ensureNotSpecialUser
+        if (targetUserHandle.getIdentifier() < 0) {
+            throw new IllegalArgumentException(
+                    "Call does not support special user " + targetUserHandle);
+        }
+
+        if (mContext.checkPermission(
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingPid, callingUid)
+                == PackageManager.PERMISSION_GRANTED) {
+            try {
+                mContext.createPackageContextAsUser(
+                        callingPackageName, /* flags= */ 0, targetUserHandle);
+            } catch (PackageManager.NameNotFoundException e) {
+                throw new SecurityException(
+                        "Package: "
+                                + callingPackageName
+                                + " haven't installed for user "
+                                + targetUserHandle.getIdentifier());
+            }
+            return targetUserHandle;
+        }
+        throw new SecurityException(
+                "Permission denied while calling from uid "
+                        + callingUid
+                        + " with "
+                        + targetUserHandle
+                        + "; Requires permission: "
+                        + Manifest.permission.INTERACT_ACROSS_USERS_FULL);
+    }
+
+    /**
+     * Checks that the caller's supposed package name matches the uid making the call.
+     *
+     * @throws SecurityException if the package name and uid don't match.
+     */
+    private void validateCallingPackageInternal(
+            int actualCallingUid, @NonNull String claimedCallingPackage) {
+        UserHandle callingUserHandle = UserHandle.getUserHandleForUid(actualCallingUid);
+        Context actualCallingUserContext = mContext.createContextAsUser(
+                callingUserHandle, /* flags= */ 0);
+        int claimedCallingUid =
+                getPackageUid(actualCallingUserContext, claimedCallingPackage);
+        if (claimedCallingUid != actualCallingUid) {
+            throw new SecurityException(
+                    "Specified calling package ["
+                            + claimedCallingPackage
+                            + "] does not match the calling uid "
+                            + actualCallingUid);
+        }
+    }
+
+    /**
+     * Finds the UID of the {@code packageName} in the given {@code context}. Returns {@link
+     * Process#INVALID_UID} if unable to find the UID.
+     */
+    private int getPackageUid(@NonNull Context context, @NonNull String packageName) {
+        try {
+            return context.getPackageManager().getPackageUid(packageName, /* flags= */ 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            return Process.INVALID_UID;
+        }
+    }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/ServiceHelper.java b/services/appfunctions/java/com/android/server/appfunctions/ServiceHelper.java
new file mode 100644
index 0000000..6cd87d3
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/ServiceHelper.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Helper interface for AppFunctionService.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public interface ServiceHelper {
+    /**
+     * Resolves the AppFunctionService for the target package.
+     *
+     * @param targetPackageName The package name of the target.
+     * @param targetUser        The user which the caller is requesting to execute as.
+     * @return The intent to bind to the target service.
+     */
+    Intent resolveAppFunctionService(@NonNull String targetPackageName,
+                                     @NonNull UserHandle targetUser);
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/ServiceHelperImpl.java b/services/appfunctions/java/com/android/server/appfunctions/ServiceHelperImpl.java
new file mode 100644
index 0000000..e49fba5
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/ServiceHelperImpl.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.app.appfunctions.AppFunctionService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.UserHandle;
+
+import java.util.Objects;
+
+class ServiceHelperImpl implements ServiceHelper {
+    private final Context mContext;
+
+    // TODO(b/357551503): Keep track of unlocked users.
+
+    ServiceHelperImpl(@NonNull Context context) {
+        mContext = Objects.requireNonNull(context);
+    }
+
+    @Override
+    public Intent resolveAppFunctionService(@NonNull String targetPackageName,
+                                            @NonNull UserHandle targetUser) {
+        Intent serviceIntent = new Intent(AppFunctionService.SERVICE_INTERFACE);
+        serviceIntent.setPackage(targetPackageName);
+        ResolveInfo resolveInfo = mContext.createContextAsUser(targetUser, /* flags= */ 0)
+                .getPackageManager().resolveService(serviceIntent, 0);
+        if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+            return null;
+        }
+
+        ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+        if (!Manifest.permission.BIND_APP_FUNCTION_SERVICE.equals(
+                serviceInfo.permission)) {
+            return null;
+        }
+        serviceIntent.setComponent(
+                new ComponentName(serviceInfo.packageName, serviceInfo.name));
+
+        return serviceIntent;
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 1be352e..0827f2a 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -24,7 +24,7 @@
 import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
-import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ACTIVITY;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
@@ -237,11 +237,11 @@
 
             @Override
             public void onActivityLaunchBlocked(int displayId,
-                    @NonNull ComponentName componentName, @UserIdInt int userId,
+                    @NonNull ComponentName componentName, @NonNull UserHandle user,
                     @Nullable IntentSender intentSender) {
                 try {
                     mActivityListener.onActivityLaunchBlocked(
-                            displayId, componentName, userId, intentSender);
+                            displayId, componentName, user, intentSender);
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
                 }
@@ -736,7 +736,7 @@
                     }
                 }
                 break;
-            case POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR:
+            case POLICY_TYPE_BLOCKED_ACTIVITY:
                 if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
                     synchronized (mVirtualDeviceLock) {
                         mDevicePolicies.put(policyType, devicePolicy);
@@ -1371,8 +1371,7 @@
             mActivityListenerAdapter.onActivityLaunchBlocked(
                     displayId,
                     activityInfo.getComponentName(),
-                    UserHandle.getUserHandleForUid(
-                            activityInfo.applicationInfo.uid).getIdentifier(),
+                    UserHandle.getUserHandleForUid(activityInfo.applicationInfo.uid),
                     intentSender);
         }
     }
@@ -1388,7 +1387,7 @@
             return true;
         }
         // Do not show the dialog if disabled by policy.
-        return getDevicePolicy(POLICY_TYPE_BLOCKED_ACTIVITY_BEHAVIOR) == DEVICE_POLICY_DEFAULT;
+        return getDevicePolicy(POLICY_TYPE_BLOCKED_ACTIVITY) == DEVICE_POLICY_DEFAULT;
     }
 
     private void onSecureWindowShown(int displayId, int uid) {
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 6333159..4aac7a0 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -1347,7 +1347,6 @@
                             bus.getStatsDuration(),
                             bus.getDischargePercentage(),
                             bus.getDischargeDurationMs());
-
             if (DBG) {
                 Slog.d(TAG, "BatteryUsageStats dump = " + bus);
             }
@@ -1357,45 +1356,25 @@
 
             final float totalDeviceConsumedPowerMah = (float) deviceConsumer.getConsumedPower();
 
-            for (@BatteryConsumer.PowerComponent int componentId = 0;
-                    componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
-                    componentId++) {
+            for (@BatteryConsumer.PowerComponentId int componentIndex :
+                    deviceConsumer.getPowerComponentIds()) {
 
                 for (@BatteryConsumer.ProcessState int processState : UID_PROCESS_STATES) {
 
-                    if (!addStatsForPredefinedComponent(
+                    if (!addStatsForPowerComponent(
                             data,
                             sessionInfo,
                             Process.INVALID_UID,
                             processState,
                             totalDeviceConsumedPowerMah,
+                            0,
                             deviceConsumer,
-                            componentId)) {
+                            componentIndex)) {
                         return StatsManager.PULL_SUCCESS;
                     }
                 }
             }
 
-            final int customPowerComponentCount = deviceConsumer.getCustomPowerComponentCount();
-            for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-                    componentId
-                            < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                                    + customPowerComponentCount;
-                    componentId++) {
-
-                if (!addStatsForCustomComponent(
-                        data,
-                        sessionInfo,
-                        Process.INVALID_UID,
-                        BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                        0,
-                        totalDeviceConsumedPowerMah,
-                        deviceConsumer,
-                        componentId)) {
-                    return StatsManager.PULL_SUCCESS;
-                }
-            }
-
             final List<UidBatteryConsumer> uidConsumers = bus.getUidBatteryConsumers();
             uidConsumers.sort(
                     Comparator.<BatteryConsumer>comparingDouble(BatteryConsumer::getConsumedPower)
@@ -1406,47 +1385,22 @@
                 final int uid = uidConsumer.getUid();
                 final float totalConsumedPowerMah = (float) uidConsumer.getConsumedPower();
 
-                for (@BatteryConsumer.PowerComponent int componentId = 0;
-                        componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
-                        componentId++) {
+                for (@BatteryConsumer.PowerComponentId int componentIndex :
+                        uidConsumer.getPowerComponentIds()) {
 
                     for (@BatteryConsumer.ProcessState int processState : UID_PROCESS_STATES) {
 
-                        if (!addStatsForPredefinedComponent(
+                        long timeInProcessStateMs = uidConsumer.getTimeInProcessStateMs(
+                                processState);
+                        if (!addStatsForPowerComponent(
                                 data,
                                 sessionInfo,
                                 uid,
                                 processState,
                                 totalConsumedPowerMah,
+                                timeInProcessStateMs,
                                 uidConsumer,
-                                componentId)) {
-                            return StatsManager.PULL_SUCCESS;
-                        }
-                    }
-                }
-
-                // looping over custom components
-                for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-                        componentId
-                                < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                                        + customPowerComponentCount;
-                        componentId++) {
-                    for (@BatteryConsumer.ProcessState int processState : UID_PROCESS_STATES) {
-                        final long timeInStateMillis =
-                                uidConsumer.getTimeInProcessStateMs(processState);
-                        if (timeInStateMillis <= 0) {
-                            continue;
-                        }
-
-                        if (!addStatsForCustomComponent(
-                                data,
-                                sessionInfo,
-                                uid,
-                                processState,
-                                timeInStateMillis,
-                                totalConsumedPowerMah,
-                                uidConsumer,
-                                componentId)) {
+                                componentIndex)) {
                             return StatsManager.PULL_SUCCESS;
                         }
                     }
@@ -1455,20 +1409,21 @@
             return StatsManager.PULL_SUCCESS;
         }
 
-        private boolean addStatsForPredefinedComponent(
+        private boolean addStatsForPowerComponent(
                 List<StatsEvent> data,
                 SessionInfo sessionInfo,
                 int uid,
                 @BatteryConsumer.ProcessState int processState,
                 float totalConsumedPowerMah,
+                long timeInState,
                 BatteryConsumer batteryConsumer,
-                @BatteryConsumer.PowerComponent int componentId) {
+                @BatteryConsumer.PowerComponentId int componentId) {
             final BatteryConsumer.Key key = batteryConsumer.getKey(componentId, processState);
             if (key == null) {
                 return true;
             }
 
-            final String powerComponentName = BatteryConsumer.powerComponentIdToString(componentId);
+            final String powerComponentName = batteryConsumer.getPowerComponentName(componentId);
             final float powerMah = (float) batteryConsumer.getConsumedPower(key);
             final long powerComponentDurationMillis = batteryConsumer.getUsageDurationMillis(key);
 
@@ -1476,13 +1431,6 @@
                 return true;
             }
 
-            long timeInState = 0;
-            if (batteryConsumer instanceof UidBatteryConsumer) {
-                timeInState =
-                        ((UidBatteryConsumer) batteryConsumer)
-                                .getTimeInProcessStateMs(processState);
-            }
-
             return addStatsAtom(
                     data,
                     sessionInfo,
@@ -1495,44 +1443,6 @@
                     powerComponentDurationMillis);
         }
 
-        private boolean addStatsForCustomComponent(
-                List<StatsEvent> data,
-                SessionInfo sessionInfo,
-                int uid,
-                @BatteryConsumer.ProcessState int processState,
-                long timeInStateMillis,
-                float totalConsumedPowerMah,
-                BatteryConsumer batteryConsumer,
-                int componentId) {
-
-            if (componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
-                throw new IllegalArgumentException("Invalid custom component id: " + componentId);
-            }
-
-            final float powerMah =
-                    (float) batteryConsumer.getConsumedPowerForCustomComponent(componentId);
-            if (powerMah == 0) {
-                return true;
-            }
-
-            final String powerComponentName =
-                    batteryConsumer.getCustomPowerComponentName(componentId);
-
-            final long powerComponentDurationMillis =
-                    batteryConsumer.getUsageDurationForCustomComponentMillis(componentId);
-
-            return addStatsAtom(
-                    data,
-                    sessionInfo,
-                    uid,
-                    processState,
-                    timeInStateMillis,
-                    powerComponentName,
-                    totalConsumedPowerMah,
-                    powerMah,
-                    powerComponentDurationMillis);
-        }
-
         /**
          * Returns true on success and false if reached max atoms capacity and no more atoms should
          * be added
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 0b6d135..9000e9b 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -215,6 +215,7 @@
         "stability",
         "statsd",
         "system_performance",
+        "system_sw_battery",
         "system_sw_touch",
         "system_sw_usb",
         "test_suites",
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index f0f6db2..35be0f3 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -183,15 +183,17 @@
             Flags::offloadDozeOverrideHoldsWakelock
     );
 
-    private final FlagState mOffloadSessionCancelBlockScreenOn =
-            new FlagState(
-                    Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON,
-                    Flags::offloadSessionCancelBlockScreenOn);
+    private final FlagState mOffloadSessionCancelBlockScreenOn = new FlagState(
+            Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON,
+            Flags::offloadSessionCancelBlockScreenOn);
 
-    private final FlagState mNewHdrBrightnessModifier =
-            new FlagState(
-                    Flags.FLAG_NEW_HDR_BRIGHTNESS_MODIFIER,
-                    Flags::newHdrBrightnessModifier);
+    private final FlagState mNewHdrBrightnessModifier = new FlagState(
+            Flags.FLAG_NEW_HDR_BRIGHTNESS_MODIFIER,
+            Flags::newHdrBrightnessModifier);
+
+    private final FlagState mIdleScreenConfigInSubscribingLightSensor = new FlagState(
+            Flags.FLAG_IDLE_SCREEN_CONFIG_IN_SUBSCRIBING_LIGHT_SENSOR,
+            Flags::idleScreenConfigInSubscribingLightSensor);
 
     private final FlagState mNormalBrightnessForDozeParameter = new FlagState(
             Flags.FLAG_NORMAL_BRIGHTNESS_FOR_DOZE_PARAMETER,
@@ -404,6 +406,14 @@
         return mNormalBrightnessForDozeParameter.isEnabled();
     }
 
+     /**
+      * @return {@code true} if idle timer refresh rate config is accounted for while subscribing to
+      * the light sensor
+      */
+    public boolean isIdleScreenConfigInSubscribingLightSensorEnabled() {
+        return mIdleScreenConfigInSubscribingLightSensor.isEnabled();
+    }
+
     /**
      * dumps all flagstates
      * @param pw printWriter
@@ -444,6 +454,7 @@
         pw.println(" " + mOffloadSessionCancelBlockScreenOn);
         pw.println(" " + mNewHdrBrightnessModifier);
         pw.println(" " + mNormalBrightnessForDozeParameter);
+        pw.println(" " + mIdleScreenConfigInSubscribingLightSensor);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index d929249..da5063a 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -330,3 +330,14 @@
     bug: "331275392"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "idle_screen_config_in_subscribing_light_sensor"
+    namespace: "display_manager"
+    description: "Account for Idle screen refresh rate configs while subscribing to light sensor"
+    bug: "358019330"
+    is_fixed_read_only: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 31f5a41..c31d1d8 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -483,6 +483,7 @@
                 /* attemptReadFromFeatureParams= */ true);
             mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig,
                 /* attemptReadFromFeatureParams= */ true);
+            mBrightnessObserver.loadIdleScreenRefreshRateConfigs(displayDeviceConfig);
             mBrightnessObserver.reloadLightSensor(displayDeviceConfig);
             mHbmObserver.setupHdrRefreshRates(displayDeviceConfig);
         }
@@ -1752,6 +1753,10 @@
         private SparseArray<RefreshRateRange> mHighZoneRefreshRateForThermals;
         private int mRefreshRateInHighZone;
 
+        @Nullable
+        private List<IdleScreenRefreshRateTimeoutLuxThresholdPoint>
+                mIdleScreenRefreshRateTimeoutLuxThresholdPoints;
+
         @GuardedBy("mLock")
         private @Temperature.ThrottlingStatus int mThermalStatus = Temperature.THROTTLING_NONE;
 
@@ -1765,6 +1770,24 @@
             mRefreshRateInHighZone = context.getResources().getInteger(
                     R.integer.config_fixedRefreshRateInHighZone);
             mVsyncLowLightBlockingVoteEnabled = flags.isVsyncLowLightVoteEnabled();
+            loadIdleScreenRefreshRateConfigs(/* displayDeviceConfig= */ null);
+        }
+
+        private void loadIdleScreenRefreshRateConfigs(DisplayDeviceConfig displayDeviceConfig) {
+            synchronized (mLock) {
+                if (!mDisplayManagerFlags.isIdleScreenConfigInSubscribingLightSensorEnabled()
+                        || displayDeviceConfig == null || displayDeviceConfig
+                        .getIdleScreenRefreshRateTimeoutLuxThresholdPoint().isEmpty()) {
+                    // Setting this to null will let surface flinger know that the idle timer is not
+                    // configured in the display configs
+                    mIdleScreenRefreshRateConfig = null;
+                    mIdleScreenRefreshRateTimeoutLuxThresholdPoints = null;
+                    return;
+                }
+                mIdleScreenRefreshRateTimeoutLuxThresholdPoints =
+                        displayDeviceConfig
+                                .getIdleScreenRefreshRateTimeoutLuxThresholdPoint();
+            }
         }
 
         /**
@@ -1813,11 +1836,19 @@
             return mRefreshRateInLowZone;
         }
 
+        @Nullable
         @VisibleForTesting
         IdleScreenRefreshRateConfig getIdleScreenRefreshRateConfig() {
             return mIdleScreenRefreshRateConfig;
         }
 
+        @Nullable
+        @VisibleForTesting
+        List<IdleScreenRefreshRateTimeoutLuxThresholdPoint>
+                getIdleScreenRefreshRateTimeoutLuxThresholdPoints() {
+            return mIdleScreenRefreshRateTimeoutLuxThresholdPoints;
+        }
+
         private void loadLowBrightnessThresholds(@Nullable DisplayDeviceConfig displayDeviceConfig,
                 boolean attemptReadFromFeatureParams) {
             loadRefreshRateInHighZone(displayDeviceConfig, attemptReadFromFeatureParams);
@@ -2212,12 +2243,11 @@
                 mShouldObserveAmbientHighChange = false;
             }
 
-            if (mShouldObserveAmbientLowChange || mShouldObserveAmbientHighChange) {
+            if (shouldRegisterLightSensor()) {
                 Sensor lightSensor = getLightSensor();
 
                 if (lightSensor != null && lightSensor != mLightSensor) {
                     final Resources res = mContext.getResources();
-
                     mAmbientFilter = AmbientFilterFactory.createBrightnessFilter(TAG, res);
                     mLightSensor = lightSensor;
                 }
@@ -2443,8 +2473,8 @@
             }
 
             boolean registerForThermals = false;
-            if ((mShouldObserveAmbientLowChange || mShouldObserveAmbientHighChange)
-                     && isDeviceActive() && !mLowPowerModeEnabled && mRefreshRateChangeable) {
+            if (shouldRegisterLightSensor() && isDeviceActive() && !mLowPowerModeEnabled
+                    && mRefreshRateChangeable) {
                 registerLightSensor();
                 registerForThermals = mLowZoneRefreshRateForThermals != null
                         || mHighZoneRefreshRateForThermals != null;
@@ -2463,6 +2493,17 @@
             }
         }
 
+        private boolean shouldRegisterLightSensor() {
+            return mShouldObserveAmbientLowChange || mShouldObserveAmbientHighChange
+                    || isIdleScreenRefreshRateConfigDefined();
+        }
+
+        private boolean isIdleScreenRefreshRateConfigDefined() {
+            return mDisplayManagerFlags.isIdleScreenConfigInSubscribingLightSensorEnabled()
+                    && mIdleScreenRefreshRateTimeoutLuxThresholdPoints != null
+                    && !mIdleScreenRefreshRateTimeoutLuxThresholdPoints.isEmpty();
+        }
+
         private void registerLightSensor() {
             if (mRegisteredLightSensor == mLightSensor) {
                 return;
@@ -2563,7 +2604,6 @@
                     // is interrupted by a new sensor event.
                     mHandler.postDelayed(mInjectSensorEventRunnable, INJECT_EVENTS_INTERVAL_MS);
                 }
-
                 if (mDisplayManagerFlags.isIdleScreenRefreshRateTimeoutEnabled()) {
                     updateIdleScreenRefreshRate(mAmbientLux);
                 }
@@ -2628,24 +2668,15 @@
         }
 
         private void updateIdleScreenRefreshRate(float ambientLux) {
-            List<IdleScreenRefreshRateTimeoutLuxThresholdPoint>
-                    idleScreenRefreshRateTimeoutLuxThresholdPoints;
-            synchronized (mLock) {
-                if (mDefaultDisplayDeviceConfig == null || mDefaultDisplayDeviceConfig
-                        .getIdleScreenRefreshRateTimeoutLuxThresholdPoint().isEmpty()) {
-                    // Setting this to null will let surface flinger know that the idle timer is not
-                    // configured in the display configs
-                    mIdleScreenRefreshRateConfig = null;
-                    return;
-                }
-
-                idleScreenRefreshRateTimeoutLuxThresholdPoints =
-                        mDefaultDisplayDeviceConfig
-                                .getIdleScreenRefreshRateTimeoutLuxThresholdPoint();
+            if (mIdleScreenRefreshRateTimeoutLuxThresholdPoints == null
+                    || mIdleScreenRefreshRateTimeoutLuxThresholdPoints.isEmpty()) {
+                mIdleScreenRefreshRateConfig = null;
+                return;
             }
+
             int newTimeout = -1;
             for (IdleScreenRefreshRateTimeoutLuxThresholdPoint point :
-                    idleScreenRefreshRateTimeoutLuxThresholdPoints) {
+                    mIdleScreenRefreshRateTimeoutLuxThresholdPoints) {
                 int newLux = point.getLux().intValue();
                 if (newLux <= ambientLux) {
                     newTimeout = point.getTimeout().intValue();
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index a8fc862..bad714f 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -466,7 +466,7 @@
                 injector.getLooper());
         mTouchpadDebugViewController =
                 touchpadVisualizer() ? new TouchpadDebugViewController(mContext,
-                        injector.getLooper()) : null;
+                        injector.getLooper(), this) : null;
         mBatteryController = new BatteryController(mContext, mNative, injector.getLooper(),
                 injector.getUEventManager());
         mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled()
@@ -1798,6 +1798,16 @@
         return mNative.getSensorList(deviceId);
     }
 
+    /**
+     * Retrieves the hardware properties of the touchpad for the given device ID.
+     * Returns null if the device has no touchpad hardware properties
+     * or if the device ID is invalid.
+     */
+    @Nullable
+    public TouchpadHardwareProperties getTouchpadHardwareProperties(int deviceId) {
+        return mNative.getTouchpadHardwareProperties(deviceId);
+    }
+
     @Override // Binder call
     public boolean registerSensorListener(IInputSensorEventListener listener) {
         if (DEBUG) {
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 69a9f4d..1e7c97f9 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -216,6 +216,9 @@
 
     InputSensorInfo[] getSensorList(int deviceId);
 
+    @Nullable
+    TouchpadHardwareProperties getTouchpadHardwareProperties(int deviceId);
+
     boolean flushSensor(int deviceId, int sensorType);
 
     boolean enableSensor(int deviceId, int sensorType, int samplingPeriodUs,
@@ -512,6 +515,9 @@
         public native InputSensorInfo[] getSensorList(int deviceId);
 
         @Override
+        public native TouchpadHardwareProperties getTouchpadHardwareProperties(int deviceId);
+
+        @Override
         public native boolean flushSensor(int deviceId, int sensorType);
 
         @Override
diff --git a/services/core/java/com/android/server/input/TouchpadHardwareProperties.java b/services/core/java/com/android/server/input/TouchpadHardwareProperties.java
new file mode 100644
index 0000000..71abb19
--- /dev/null
+++ b/services/core/java/com/android/server/input/TouchpadHardwareProperties.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input;
+
+import com.android.internal.util.DataClass;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByNative;
+
+/**
+ * A Java representation of hardware properties for a touchpad or mouse device.
+ * This class mirrors the Gestures library HardwareProperties C++ struct used for representing
+ * touchpad and mouse device properties, including touch area, resolution, and features like haptic
+ * feedback, multitouch, and scroll wheels. It facilitates interaction between native and managed
+ * code in Android.
+ */
+@DataClass(
+        genToString = true
+)
+@UsedByNative(
+        description = "Called from JNI in jni/com_android_server_input_InputManagerService.cpp",
+        kind = KeepItemKind.CLASS_AND_MEMBERS)
+public class TouchpadHardwareProperties {
+    /**
+     * The minimum X coordinate that the device can report.
+     */
+    private float mLeft;
+
+    /**
+     * The minimum Y coordinate that the device can report.
+     */
+    private float mTop;
+
+    /**
+     * The maximum X coordinate that the device can report.
+     */
+    private float mRight;
+
+    /**
+     * The maximum Y coordinate that the device can report.
+     */
+    private float mBottom;
+
+    /**
+     * The resolution of the X axis, in units per mm. Set to 0 if the
+     * resolution is unknown.
+     */
+    private float mResX;
+    /**
+     * The resolutions of the Y axis, in units per mm. Set to 0 if the
+     * resolution is unknown.
+     */
+    private float mResY;
+
+    /**
+     * The minimum orientation value.
+     */
+    private float mOrientationMinimum;
+    /**
+     * The maximum orientation value.
+     */
+    private float mOrientationMaximum;
+
+    /**
+     * The maximum number of finger slots that the device can report in one
+     * HardwareState struct.
+     */
+    private short mMaxFingerCount;
+
+    /**
+     * Whether the touchpad has a button under its touch surface, allowing the
+     * user to click by pressing (almost) anywhere on the pad, as opposed to
+     * having one or more separate buttons for clicking.
+     */
+    private boolean mIsButtonPad;
+
+    /**
+     * Whether the touchpad is haptic, meaning that it reports true pressure (not
+     * just touch area) via the pressure axis, and can provide haptic feedback.
+     */
+    private boolean mIsHapticPad;
+
+    /**
+     * Whether the touchpad reports pressure values in any way.
+     */
+    private boolean mReportsPressure = true;
+
+    /**
+     * Returns a string representation of this instance, including all fields.
+     */
+    public String toString() {
+        return "HardwareProperties{"
+                + "left=" + mLeft
+                + ", top=" + mTop
+                + ", right=" + mRight
+                + ", bottom=" + mBottom
+                + ", resX=" + mResX
+                + ", resY=" + mResY
+                + ", orientationMinimum=" + mOrientationMinimum
+                + ", orientationMaximum=" + mOrientationMaximum
+                + ", maxFingerCount=" + mMaxFingerCount
+                + ", isButtonPad=" + mIsButtonPad
+                + ", isHapticPad=" + mIsHapticPad
+                + ", reportsPressure=" + mReportsPressure
+                + '}';
+    }
+
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/services/core/java/com/android/server/input
+    // /TouchpadHardwareProperties.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    @DataClass.Generated.Member
+    /* package-private */ TouchpadHardwareProperties(
+            float left,
+            float top,
+            float right,
+            float bottom,
+            float resX,
+            float resY,
+            float orientationMinimum,
+            float orientationMaximum,
+            short maxFingerCount,
+            boolean isButtonPad,
+            boolean isHapticPad,
+            boolean reportsPressure) {
+        this.mLeft = left;
+        this.mTop = top;
+        this.mRight = right;
+        this.mBottom = bottom;
+        this.mResX = resX;
+        this.mResY = resY;
+        this.mOrientationMinimum = orientationMinimum;
+        this.mOrientationMaximum = orientationMaximum;
+        this.mMaxFingerCount = maxFingerCount;
+        this.mIsButtonPad = isButtonPad;
+        this.mIsHapticPad = isHapticPad;
+        this.mReportsPressure = reportsPressure;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    /**
+     * The minimum X coordinate that the device can report.
+     */
+    @DataClass.Generated.Member
+    public float getLeft() {
+        return mLeft;
+    }
+
+    /**
+     * The minimum Y coordinate that the device can report.
+     */
+    @DataClass.Generated.Member
+    public float getTop() {
+        return mTop;
+    }
+
+    /**
+     * The maximum X coordinate that the device can report.
+     */
+    @DataClass.Generated.Member
+    public float getRight() {
+        return mRight;
+    }
+
+    /**
+     * The maximum Y coordinate that the device can report.
+     */
+    @DataClass.Generated.Member
+    public float getBottom() {
+        return mBottom;
+    }
+
+    /**
+     * The resolution of the X axis, in units per mm. Set to 0 if the
+     * resolution is unknown.
+     */
+    @DataClass.Generated.Member
+    public float getResX() {
+        return mResX;
+    }
+
+    /**
+     * The resolutions of the Y axis, in units per mm. Set to 0 if the
+     * resolution is unknown.
+     */
+    @DataClass.Generated.Member
+    public float getResY() {
+        return mResY;
+    }
+
+    /**
+     * The minimum orientation value.
+     */
+    @DataClass.Generated.Member
+    public float getOrientationMinimum() {
+        return mOrientationMinimum;
+    }
+
+    /**
+     * The maximum orientation value.
+     */
+    @DataClass.Generated.Member
+    public float getOrientationMaximum() {
+        return mOrientationMaximum;
+    }
+
+    /**
+     * The maximum number of finger slots that the device can report in one
+     * HardwareState struct.
+     */
+    @DataClass.Generated.Member
+    public short getMaxFingerCount() {
+        return mMaxFingerCount;
+    }
+
+    /**
+     * Whether the touchpad has a button under its touch surface, allowing the
+     * user to click by pressing (almost) anywhere on the pad, as opposed to
+     * having one or more separate buttons for clicking.
+     */
+    @DataClass.Generated.Member
+    public boolean isIsButtonPad() {
+        return mIsButtonPad;
+    }
+
+    /**
+     * Whether the touchpad is haptic, meaning that it reports true pressure (not
+     * just touch area) via the pressure axis, and can provide haptic feedback.
+     */
+    @DataClass.Generated.Member
+    public boolean isIsHapticPad() {
+        return mIsHapticPad;
+    }
+
+    /**
+     * Whether the touchpad reports pressure values in any way.
+     */
+    @DataClass.Generated.Member
+    public boolean isReportsPressure() {
+        return mReportsPressure;
+    }
+
+    /**
+     * A builder for {@link TouchpadHardwareProperties}
+     */
+    @SuppressWarnings("WeakerAccess")
+    @DataClass.Generated.Member
+    public static class Builder {
+
+        private float mLeft;
+        private float mTop;
+        private float mRight;
+        private float mBottom;
+        private float mResX;
+        private float mResY;
+        private float mOrientationMinimum;
+        private float mOrientationMaximum;
+        private short mMaxFingerCount;
+        private boolean mIsButtonPad;
+        private boolean mIsHapticPad;
+        private boolean mReportsPressure;
+
+        private long mBuilderFieldsSet = 0L;
+
+        /**
+         * Creates a new Builder.
+         *
+         * @param left
+         *   The minimum X coordinate that the device can report.
+         * @param top
+         *   The minimum Y coordinate that the device can report.
+         * @param right
+         *   The maximum X coordinate that the device can report.
+         * @param bottom
+         *   The maximum Y coordinate that the device can report.
+         * @param resX
+         *   The resolution of the X axis, in units per mm. Set to 0 if the
+         *   resolution is unknown.
+         * @param resY
+         *   The resolutions of the Y axis, in units per mm. Set to 0 if the
+         *   resolution is unknown.
+         * @param orientationMinimum
+         *   The minimum orientation value.
+         * @param orientationMaximum
+         *   The maximum orientation value.
+         * @param maxFingerCount
+         *   The maximum number of finger slots that the device can report in one
+         *   HardwareState struct.
+         * @param isButtonPad
+         *   Whether the touchpad has a button under its touch surface, allowing the
+         *   user to click by pressing (almost) anywhere on the pad, as opposed to
+         *   having one or more separate buttons for clicking.
+         * @param isHapticPad
+         *   Whether the touchpad is haptic, meaning that it reports true pressure (not
+         *   just touch area) via the pressure axis, and can provide haptic feedback.
+         */
+        public Builder(
+                float left,
+                float top,
+                float right,
+                float bottom,
+                float resX,
+                float resY,
+                float orientationMinimum,
+                float orientationMaximum,
+                short maxFingerCount,
+                boolean isButtonPad,
+                boolean isHapticPad) {
+            mLeft = left;
+            mTop = top;
+            mRight = right;
+            mBottom = bottom;
+            mResX = resX;
+            mResY = resY;
+            mOrientationMinimum = orientationMinimum;
+            mOrientationMaximum = orientationMaximum;
+            mMaxFingerCount = maxFingerCount;
+            mIsButtonPad = isButtonPad;
+            mIsHapticPad = isHapticPad;
+        }
+
+        /**
+         * The minimum X coordinate that the device can report.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setLeft(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x1;
+            mLeft = value;
+            return this;
+        }
+
+        /**
+         * The minimum Y coordinate that the device can report.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setTop(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x2;
+            mTop = value;
+            return this;
+        }
+
+        /**
+         * The maximum X coordinate that the device can report.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setRight(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x4;
+            mRight = value;
+            return this;
+        }
+
+        /**
+         * The maximum Y coordinate that the device can report.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setBottom(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x8;
+            mBottom = value;
+            return this;
+        }
+
+        /**
+         * The resolution of the X axis, in units per mm. Set to 0 if the
+         * resolution is unknown.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setResX(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x10;
+            mResX = value;
+            return this;
+        }
+
+        /**
+         * The resolutions of the Y axis, in units per mm. Set to 0 if the
+         * resolution is unknown.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setResY(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x20;
+            mResY = value;
+            return this;
+        }
+
+        /**
+         * The minimum orientation value.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setOrientationMinimum(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x40;
+            mOrientationMinimum = value;
+            return this;
+        }
+
+        /**
+         * The maximum orientation value.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setOrientationMaximum(float value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x80;
+            mOrientationMaximum = value;
+            return this;
+        }
+
+        /**
+         * The maximum number of finger slots that the device can report in one
+         * HardwareState struct.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setMaxFingerCount(short value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x100;
+            mMaxFingerCount = value;
+            return this;
+        }
+
+        /**
+         * Whether the touchpad has a button under its touch surface, allowing the
+         * user to click by pressing (almost) anywhere on the pad, as opposed to
+         * having one or more separate buttons for clicking.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setIsButtonPad(boolean value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x200;
+            mIsButtonPad = value;
+            return this;
+        }
+
+        /**
+         * Whether the touchpad is haptic, meaning that it reports true pressure (not
+         * just touch area) via the pressure axis, and can provide haptic feedback.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setIsHapticPad(boolean value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x400;
+            mIsHapticPad = value;
+            return this;
+        }
+
+        /**
+         * Whether the touchpad reports pressure values in any way.
+         */
+        @DataClass.Generated.Member
+        public @android.annotation.NonNull Builder setReportsPressure(boolean value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x800;
+            mReportsPressure = value;
+            return this;
+        }
+
+        /** Builds the instance. This builder should not be touched after calling this! */
+        public @android.annotation.NonNull TouchpadHardwareProperties build() {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x1000; // Mark builder used
+
+            if ((mBuilderFieldsSet & 0x800) == 0) {
+                mReportsPressure = true;
+            }
+            TouchpadHardwareProperties o = new TouchpadHardwareProperties(
+                    mLeft,
+                    mTop,
+                    mRight,
+                    mBottom,
+                    mResX,
+                    mResY,
+                    mOrientationMinimum,
+                    mOrientationMaximum,
+                    mMaxFingerCount,
+                    mIsButtonPad,
+                    mIsHapticPad,
+                    mReportsPressure);
+            return o;
+        }
+
+        private void checkNotUsed() {
+            if ((mBuilderFieldsSet & 0x1000) != 0) {
+                throw new IllegalStateException(
+                        "This Builder should not be reused. Use a new Builder instance instead");
+            }
+        }
+    }
+
+    @DataClass.Generated(
+            time = 1723570664889L,
+            codegenVersion = "1.0.23",
+            sourceFile = "frameworks/base/services/core"
+                    + "/java/com/android/server/input/TouchpadHardwareProperties.java",
+            inputSignatures = "private  float mLeft\nprivate  float mTop\nprivate  float mRight\n"
+                    + "private  float mBottom\nprivate  float mResX\nprivate  float mResY\n"
+                    + "private  float mOrientationMinimum\nprivate  float mOrientationMaximum\n"
+                    + "private  short mMaxFingerCount\nprivate  boolean mIsButtonPad\n"
+                    + "private  boolean mIsHapticPad\nprivate  boolean mReportsPressure\n"
+                    + "public  java.lang.String toString()\n"
+                    + "class TouchpadHardwareProperties extends java.lang.Object implements []\n"
+                    + "@com.android.internal.util.DataClass(genToString=true)")
+    @Deprecated
+    private void __metadata() {}
+
+    //@formatter:on
+    // End of generated code
+}
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index 9c2aa36..c7760c6 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -29,6 +29,9 @@
 import android.view.InputDevice;
 import android.view.WindowManager;
 
+import com.android.server.input.InputManagerService;
+import com.android.server.input.TouchpadHardwareProperties;
+
 import java.util.Objects;
 
 public class TouchpadDebugViewController {
@@ -39,13 +42,16 @@
     private final Handler mHandler;
     @Nullable
     private TouchpadDebugView mTouchpadDebugView;
+    private final InputManagerService mInputManagerService;
 
-    public TouchpadDebugViewController(Context context, Looper looper) {
+    public TouchpadDebugViewController(Context context, Looper looper,
+                                       InputManagerService inputManagerService) {
         final DisplayManager displayManager = Objects.requireNonNull(
                 context.getSystemService(DisplayManager.class));
         final Display defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
         mContext = context.createDisplayContext(defaultDisplay);
         mHandler = new Handler(looper);
+        mInputManagerService = inputManagerService;
     }
 
     public void systemRunning() {
@@ -110,6 +116,17 @@
 
         wm.addView(mTouchpadDebugView, lp);
         Slog.d(TAG, "Touchpad debug view created.");
+
+        TouchpadHardwareProperties mTouchpadHardwareProperties =
+                mInputManagerService.getTouchpadHardwareProperties(
+                        touchpadId);
+        // TODO(b/360137366): Use the hardware properties to initialise layout parameters.
+        if (mTouchpadHardwareProperties != null) {
+            Slog.d(TAG, mTouchpadHardwareProperties.toString());
+        } else {
+            Slog.w(TAG, "Failed to retrieve touchpad hardware properties for "
+                    + "device ID: " + touchpadId);
+        }
     }
 
     private void hideDebugView(int touchpadId) {
@@ -122,4 +139,4 @@
         mTouchpadDebugView = null;
         Slog.d(TAG, "Touchpad debug view removed.");
     }
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 3654283..f34b4e9 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4233,6 +4233,9 @@
             @NonNull UserData userData) {
         final var bindingController = userData.mBindingController;
         final var currentImi = bindingController.getSelectedMethod();
+        if (currentImi == null) {
+            return false;
+        }
         final ImeSubtypeListItem nextSubtype = userData.mSwitchingController
                 .getNextInputMethodLocked(onlyCurrentIme, currentImi,
                         bindingController.getCurrentSubtype(),
@@ -4250,6 +4253,9 @@
     private boolean shouldOfferSwitchingToNextInputMethodLocked(@NonNull UserData userData) {
         final var bindingController = userData.mBindingController;
         final var currentImi = bindingController.getSelectedMethod();
+        if (currentImi == null) {
+            return false;
+        }
         final ImeSubtypeListItem nextSubtype = userData.mSwitchingController
                 .getNextInputMethodLocked(false /* onlyCurrentIme */, currentImi,
                         bindingController.getCurrentSubtype(),
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index 202543c..96b3e08 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -686,11 +686,8 @@
      */
     @Nullable
     public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
-            @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+            @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
             @SwitchMode int mode, boolean forward) {
-        if (imi == null) {
-            return null;
-        }
         if (Flags.imeSwitcherRevamp()) {
             return mRotationList.next(imi, subtype, onlyCurrentIme,
                     isRecency(mode, forward), forward);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index c7c984b..ffb2bb6 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5675,7 +5675,7 @@
             // a "normal" rule, it must provide a CP/ConfigActivity too.
             if (android.app.Flags.modesApi()) {
                 boolean isImplicitRuleUpdateFromSystem = updateId != null
-                        && ZenModeHelper.isImplicitRuleId(updateId)
+                        && ZenModeConfig.isImplicitRuleId(updateId)
                         && isCallerSystemOrSystemUi();
                 if (!isImplicitRuleUpdateFromSystem
                         && rule.getOwner() == null
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 0f50260..ee3f48d 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -39,6 +39,7 @@
 import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
 import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE;
 import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
+import static android.service.notification.ZenModeConfig.implicitRuleId;
 
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 import static com.android.internal.util.Preconditions.checkArgument;
@@ -155,8 +156,6 @@
     static final int RULE_LIMIT_PER_PACKAGE = 100;
     private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30);
 
-    private static final String IMPLICIT_RULE_ID_PREFIX = "implicit_"; // + pkg_name
-
     private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000;
 
     /**
@@ -783,14 +782,6 @@
         return rule;
     }
 
-    private static String implicitRuleId(String forPackage) {
-        return IMPLICIT_RULE_ID_PREFIX + forPackage;
-    }
-
-    static boolean isImplicitRuleId(@NonNull String ruleId) {
-        return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX);
-    }
-
     boolean removeAutomaticZenRule(String id, @ConfigOrigin int origin, String reason,
             int callingUid) {
         checkManageRuleOrigin("removeAutomaticZenRule", origin);
@@ -977,7 +968,16 @@
                         rule.setConditionOverride(OVERRIDE_DEACTIVATE);
                     }
                 }
+            } else if (origin == ORIGIN_USER_IN_APP && condition != null
+                    && condition.source == SOURCE_USER_ACTION) {
+                // Remove override and just apply the condition. Since the app is reporting that the
+                // user asked for it, by definition it knows that, and will adjust its automatic
+                // behavior accordingly -> no need to override.
+                rule.condition = condition;
+                rule.resetConditionOverride();
             } else {
+                // Update the condition, and check whether we can remove the override (if automatic
+                // and manual decisions agree).
                 rule.condition = condition;
                 rule.reconsiderConditionOverride();
             }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 934feb3..ba3de33 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3422,7 +3422,7 @@
                 return handleHomeShortcuts(focusedToken, event);
             case KeyEvent.KEYCODE_RECENT_APPS:
                 if (firstDown) {
-                    toggleRecentApps();
+                    showRecentApps(false /* triggeredFromAltTab */);
                     notifyKeyGestureCompleted(event,
                             KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS);
                 }
diff --git a/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerCalculator.java b/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerCalculator.java
index 5b7467e..c1f2ae8 100644
--- a/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerCalculator.java
@@ -62,7 +62,7 @@
                     builder.getAggregateBatteryConsumerBuilder(
                             BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
             for (int i = 0; i < customEnergyConsumerPowerMah.length; i++) {
-                deviceBatteryConsumerBuilder.setConsumedPowerForCustomComponent(
+                deviceBatteryConsumerBuilder.setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + i,
                         customEnergyConsumerPowerMah[i]);
             }
@@ -72,7 +72,7 @@
                     builder.getAggregateBatteryConsumerBuilder(
                             BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS);
             for (int i = 0; i < totalAppPowerMah.length; i++) {
-                appsBatteryConsumerBuilder.setConsumedPowerForCustomComponent(
+                appsBatteryConsumerBuilder.setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + i,
                         totalAppPowerMah[i]);
             }
@@ -96,7 +96,7 @@
                 newTotalPowerMah = totalPowerMah;
             }
             for (int i = 0; i < customEnergyConsumerPowerMah.length; i++) {
-                app.setConsumedPowerForCustomComponent(
+                app.setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + i,
                         customEnergyConsumerPowerMah[i]);
                 if (!app.isVirtualUid()) {
diff --git a/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
index 6820197..9506741 100644
--- a/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
+++ b/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
@@ -172,6 +172,9 @@
 
     void setUidStats(int uid, int[] states, long[] values) {
         UidStats uidStats = getUidStats(uid);
+        if (uidStats.stats == null) {
+            createUidStats(uidStats, mPowerStatsTimestamp);
+        }
         uidStats.stats.setStats(states, values);
     }
 
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
index 081e560..c734f68 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
@@ -46,6 +46,10 @@
         mHistory = history;
     }
 
+    public AggregatedPowerStatsConfig getConfig() {
+        return mAggregatedPowerStatsConfig;
+    }
+
     void setPowerComponentEnabled(int powerComponentId, boolean enabled) {
         synchronized (this) {
             if (mStats != null) {
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
index 281faf1..c5bed24 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
@@ -16,12 +16,14 @@
 
 package com.android.server.power.stats;
 
+import android.annotation.Nullable;
 import android.os.AggregateBatteryConsumer;
 import android.os.BatteryConsumer;
 import android.os.BatteryUsageStats;
 import android.os.UidBatteryConsumer;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.PowerStats;
 
 import java.util.ArrayList;
@@ -59,7 +61,7 @@
      */
     public void exportAggregatedPowerStats(BatteryUsageStats.Builder batteryUsageStatsBuilder,
             long monotonicStartTime, long monotonicEndTime) {
-        synchronized (this) {
+        synchronized (mPowerStatsAggregator) {
             boolean hasStoredSpans = false;
             long maxEndTime = monotonicStartTime;
             List<PowerStatsSpan.Metadata> spans = mPowerStatsStore.getTableOfContents();
@@ -116,7 +118,8 @@
         }
     }
 
-    private void populateBatteryUsageStatsBuilder(
+    @VisibleForTesting
+    void populateBatteryUsageStatsBuilder(
             BatteryUsageStats.Builder batteryUsageStatsBuilder, AggregatedPowerStats stats) {
         List<PowerComponentAggregatedPowerStats> powerComponentStats =
                 stats.getPowerComponentStats();
@@ -125,15 +128,17 @@
         }
     }
 
-    private static void populateBatteryUsageStatsBuilder(
+    private void populateBatteryUsageStatsBuilder(
             BatteryUsageStats.Builder batteryUsageStatsBuilder,
             PowerComponentAggregatedPowerStats powerComponentStats) {
         PowerStats.Descriptor descriptor = powerComponentStats.getPowerStatsDescriptor();
         if (descriptor == null) {
             return;
         }
-        boolean isCustomComponent =
-                descriptor.powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
+
+        if (!batteryUsageStatsBuilder.isSupportedPowerComponent(descriptor.powerComponentId)) {
+            return;
+        }
 
         PowerStatsLayout layout = new PowerStatsLayout();
         layout.fromExtras(descriptor.extras);
@@ -149,16 +154,17 @@
             }
 
             for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) {
-                if (batteryUsageStatsBuilder.isPowerStateDataNeeded() && !isCustomComponent) {
-                    if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) {
-                        continue;
+                if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) {
+                    if (powerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+                        populateAggregatedBatteryConsumer(batteryUsageStatsBuilder,
+                                powerComponentStats,
+                                layout, deviceStats, screenState, powerState);
                     }
-                } else if (powerState != BatteryConsumer.POWER_STATE_BATTERY) {
-                    continue;
+                } else if (powerState == BatteryConsumer.POWER_STATE_BATTERY) {
+                    populateAggregatedBatteryConsumer(batteryUsageStatsBuilder,
+                            powerComponentStats,
+                            layout, deviceStats, screenState, powerState);
                 }
-
-                populateAggregatedBatteryConsumer(batteryUsageStatsBuilder, powerComponentStats,
-                        layout, deviceStats, screenState, powerState);
             }
         }
         if (layout.isUidPowerAttributionSupported()) {
@@ -167,15 +173,12 @@
         }
     }
 
-    private static void populateAggregatedBatteryConsumer(
+    private void populateAggregatedBatteryConsumer(
             BatteryUsageStats.Builder batteryUsageStatsBuilder,
             PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout,
             long[] deviceStats, @BatteryConsumer.ScreenState int screenState,
             @BatteryConsumer.PowerState int powerState) {
         int powerComponentId = powerComponentStats.powerComponentId;
-        boolean isCustomComponent =
-                powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-
         double[] totalPower = new double[1];
         MultiStateStats.States.forEachTrackedStateCombination(
                 powerComponentStats.getConfig().getDeviceStateConfig(),
@@ -194,38 +197,27 @@
         AggregateBatteryConsumer.Builder deviceScope =
                 batteryUsageStatsBuilder.getAggregateBatteryConsumerBuilder(
                         BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
-        if (isCustomComponent) {
-            if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(powerComponentId)) {
-                deviceScope.addConsumedPowerForCustomComponent(powerComponentId, totalPower[0]);
-            }
-        } else {
-            BatteryConsumer.Key key = deviceScope.getKey(powerComponentId,
-                    BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState);
-            if (key != null) {
-                deviceScope.addConsumedPower(key, totalPower[0],
-                        BatteryConsumer.POWER_MODEL_UNDEFINED);
-            }
-            deviceScope.addConsumedPower(powerComponentId, totalPower[0],
+        BatteryConsumer.Key key = getKeyForPartialTotal(batteryUsageStatsBuilder, deviceScope,
+                powerComponentId, screenState, powerState);
+        if (key != null) {
+            deviceScope.addConsumedPower(key, totalPower[0],
                     BatteryConsumer.POWER_MODEL_UNDEFINED);
         }
+        deviceScope.addConsumedPower(powerComponentId, totalPower[0],
+                BatteryConsumer.POWER_MODEL_UNDEFINED);
     }
 
-    private static void populateBatteryConsumers(
+    private void populateBatteryConsumers(
             BatteryUsageStats.Builder batteryUsageStatsBuilder,
             PowerComponentAggregatedPowerStats powerComponentStats,
             PowerStatsLayout layout) {
         AggregatedPowerStatsConfig.PowerComponent powerComponent = powerComponentStats.getConfig();
-        int powerComponentId = powerComponent.getPowerComponentId();
-        boolean isCustomComponent =
-                powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
         PowerStats.Descriptor descriptor = powerComponentStats.getPowerStatsDescriptor();
         long[] uidStats = new long[descriptor.uidStatsArrayLength];
 
-        // TODO(b/347101393): add support for per-procstate breakdown for custom energy consumers
         boolean breakDownByProcState = batteryUsageStatsBuilder.isProcessStateDataNeeded()
                 && powerComponent
-                .getUidStateConfig()[AggregatedPowerStatsConfig.STATE_PROCESS_STATE].isTracked()
-                && !isCustomComponent;
+                .getUidStateConfig()[AggregatedPowerStatsConfig.STATE_PROCESS_STATE].isTracked();
 
         ArrayList<Integer> uids = new ArrayList<>();
         powerComponentStats.collectUids(uids);
@@ -239,7 +231,7 @@
             }
 
             for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) {
-                if (batteryUsageStatsBuilder.isPowerStateDataNeeded() && !isCustomComponent) {
+                if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) {
                     if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) {
                         continue;
                     }
@@ -254,14 +246,20 @@
         }
     }
 
-    private static void populateUidBatteryConsumers(
+    private void populateUidBatteryConsumers(
             BatteryUsageStats.Builder batteryUsageStatsBuilder,
             PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout,
             List<Integer> uids, AggregatedPowerStatsConfig.PowerComponent powerComponent,
             long[] uidStats, boolean breakDownByProcState,
             @BatteryConsumer.ScreenState int screenState,
             @BatteryConsumer.PowerState int powerState) {
-        int powerComponentId = powerComponentStats.powerComponentId;
+        if (!batteryUsageStatsBuilder.isPowerStateDataNeeded()
+                && powerState != BatteryConsumer.POWER_STATE_BATTERY) {
+            return;
+        }
+
+        @BatteryConsumer.PowerComponentId int powerComponentId =
+                powerComponentStats.powerComponentId;
         double[] powerByProcState =
                 new double[breakDownByProcState ? BatteryConsumer.PROCESS_STATE_COUNT : 1];
         double powerAllApps = 0;
@@ -283,63 +281,81 @@
                         }
 
                         double power = layout.getUidPowerEstimate(uidStats);
-                        int procState = breakDownByProcState
-                                ? states[AggregatedPowerStatsConfig.STATE_PROCESS_STATE]
-                                : BatteryConsumer.PROCESS_STATE_UNSPECIFIED;
-                        powerByProcState[procState] += power;
+                        if (breakDownByProcState) {
+                            int procState = states[AggregatedPowerStatsConfig.STATE_PROCESS_STATE];
+                            // There is a difference in how PowerComponentAggregatedPowerStats
+                            // and BatteryUsageStats see the "unspecified" process state.
+                            // PowerComponentAggregatedPowerStats preserves it as is.
+                            // BatteryUsageStats uses PROCESS_STATE_UNSPECIFIED to hold the total
+                            // across all states, and PROCESS_STATE_UNSPECIFIED is treated
+                            // the same as PROCESS_STATE_BACKGROUND, which makes sense since
+                            // PROCESS_STATE_UNSPECIFIED is only present for headless processes
+                            // like Process.ROOT_UID, Process.WIFI_UID etc.
+                            if (procState == BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+                                procState = BatteryConsumer.PROCESS_STATE_BACKGROUND;
+                            }
+                            powerByProcState[procState] += power;
+                        }
+                        powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED] += power;
                     });
 
-            double powerAllProcStates = 0;
+            int resultScreenState = batteryUsageStatsBuilder.isScreenStateDataNeeded()
+                    ? screenState : BatteryConsumer.SCREEN_STATE_UNSPECIFIED;
+            int resultPowerState = batteryUsageStatsBuilder.isPowerStateDataNeeded()
+                    ? powerState : BatteryConsumer.POWER_STATE_UNSPECIFIED;
             for (int procState = 0; procState < powerByProcState.length; procState++) {
                 double power = powerByProcState[procState];
                 if (power == 0) {
                     continue;
                 }
-                powerAllProcStates += power;
-                if (breakDownByProcState
-                        && procState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
-                    if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) {
-                        builder.addConsumedPower(
-                                builder.getKey(powerComponentId, procState, screenState,
-                                        powerState),
-                                power, BatteryConsumer.POWER_MODEL_UNDEFINED);
-                    } else {
-                        builder.addConsumedPower(
-                                builder.getKey(powerComponentId, procState, screenState,
-                                        BatteryConsumer.POWER_STATE_UNSPECIFIED),
-                                power, BatteryConsumer.POWER_MODEL_UNDEFINED);
-                    }
-                }
+                BatteryConsumer.Key key = builder.getKey(powerComponentId, procState,
+                        resultScreenState, resultPowerState);
+                builder.addConsumedPower(key, power, BatteryConsumer.POWER_MODEL_UNDEFINED);
             }
-            if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
-                if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(powerComponentId)) {
-                    builder.addConsumedPowerForCustomComponent(powerComponentId,
-                            powerAllProcStates);
-                }
-            } else {
-                builder.addConsumedPower(powerComponentId, powerAllProcStates,
+
+            if (resultScreenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED
+                    || resultPowerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+                builder.addConsumedPower(powerComponentId,
+                        powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED],
                         BatteryConsumer.POWER_MODEL_UNDEFINED);
             }
-            powerAllApps += powerAllProcStates;
+            powerAllApps += powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED];
         }
 
         AggregateBatteryConsumer.Builder allAppsScope =
                 batteryUsageStatsBuilder.getAggregateBatteryConsumerBuilder(
                         BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS);
-        if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
-            if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(powerComponentId)) {
-                allAppsScope.addConsumedPowerForCustomComponent(powerComponentId, powerAllApps);
-            }
-        } else {
-            BatteryConsumer.Key key = allAppsScope.getKey(powerComponentId,
-                    BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState);
-            if (key != null) {
+        BatteryConsumer.Key key = getKeyForPartialTotal(batteryUsageStatsBuilder, allAppsScope,
+                powerComponentId, screenState, powerState);
+        if (key != null) {
                 allAppsScope.addConsumedPower(key, powerAllApps,
                         BatteryConsumer.POWER_MODEL_UNDEFINED);
-            }
-            allAppsScope.addConsumedPower(powerComponentId, powerAllApps,
-                    BatteryConsumer.POWER_MODEL_UNDEFINED);
         }
+        allAppsScope.addConsumedPower(powerComponentId, powerAllApps,
+                BatteryConsumer.POWER_MODEL_UNDEFINED);
+    }
+
+    @Nullable
+    private BatteryConsumer.Key getKeyForPartialTotal(
+            BatteryUsageStats.Builder batteryUsageStatsBuilder,
+            AggregateBatteryConsumer.Builder builder,
+            @BatteryConsumer.PowerComponentId int powerComponentId,
+            @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState) {
+        if (!batteryUsageStatsBuilder.isScreenStateDataNeeded()) {
+            screenState = BatteryConsumer.SCREEN_STATE_UNSPECIFIED;
+        }
+        if (!batteryUsageStatsBuilder.isPowerStateDataNeeded()) {
+            powerState = BatteryConsumer.POWER_STATE_UNSPECIFIED;
+        }
+
+        if (screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED
+                && powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+            return null;
+        }
+
+        return builder.getKey(powerComponentId, BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
+                screenState, powerState);
     }
 
     private static boolean areMatchingStates(int[] states,
diff --git a/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java
index 908c751..8fb1fd6 100644
--- a/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java
@@ -16,7 +16,7 @@
 
 package com.android.server.power.stats;
 
-import static android.os.BatteryConsumer.PROCESS_STATE_ANY;
+import static android.os.BatteryConsumer.PROCESS_STATE_UNSPECIFIED;
 
 import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_AMBIENT;
 import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL;
@@ -197,7 +197,7 @@
             List<Integer> uids) {
         int[] uidStateValues = new int[stats.getConfig().getUidStateConfig().length];
         uidStateValues[STATE_SCREEN] = SCREEN_STATE_ON;
-        uidStateValues[STATE_PROCESS_STATE] = PROCESS_STATE_ANY;
+        uidStateValues[STATE_PROCESS_STATE] = PROCESS_STATE_UNSPECIFIED;
 
         for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) {
             UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i);
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index c543b6d..cda86fa 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -500,6 +500,7 @@
                     && parentInfo != null
                     && parentInfo.id == mCurrentUserId) {
                 // only the children of the current user can be started in background
+                mCurrentUserId = userId;
                 startProfileLocked(userId);
             }
         }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 0bd8441..530c03f 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3292,6 +3292,12 @@
             return false;
         }
 
+        // Check if activity is top activity of its task fragment - this prevents any trampolines
+        // followed by enterPictureInPictureMode() calls by an activity from below in its stack.
+        if (getTaskFragment() == null || getTaskFragment().getTopNonFinishingActivity() != this) {
+            return false;
+        }
+
         // Check to see if PiP is supported for the display this container is on.
         if (mDisplayContent != null && !mDisplayContent.mDwpcHelper.isEnteringPipAllowed(
                 getUid())) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 07d39d9..aa6c13e 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -44,6 +44,7 @@
 #include <batteryservice/include/batteryservice/BatteryServiceConstants.h>
 #include <binder/IServiceManager.h>
 #include <com_android_input_flags.h>
+#include <include/gestures.h>
 #include <input/Input.h>
 #include <input/PointerController.h>
 #include <input/PrintTools.h>
@@ -217,6 +218,23 @@
     jmethodID init;
 } gInputSensorInfo;
 
+static struct TouchpadHardwarePropertiesOffsets {
+    jclass clazz;
+    jmethodID constructor;
+    jfieldID left;
+    jfieldID top;
+    jfieldID right;
+    jfieldID bottom;
+    jfieldID resX;
+    jfieldID resY;
+    jfieldID orientationMinimum;
+    jfieldID orientationMaximum;
+    jfieldID maxFingerCount;
+    jfieldID isButtonPad;
+    jfieldID isHapticPad;
+    jfieldID reportsPressure;
+} gTouchpadHardwarePropertiesOffsets;
+
 // --- Global functions ---
 
 template<typename T>
@@ -2632,6 +2650,45 @@
     return arr;
 }
 
+static jobject nativeGetTouchpadHardwareProperties(JNIEnv* env, jobject nativeImplObj,
+                                                   jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    std::optional<HardwareProperties> touchpadHardwareProperties =
+            im->getInputManager()->getReader().getTouchpadHardwareProperties(deviceId);
+
+    jobject hwPropsObj = env->NewObject(gTouchpadHardwarePropertiesOffsets.clazz,
+                                        gTouchpadHardwarePropertiesOffsets.constructor);
+    if (hwPropsObj == NULL || !touchpadHardwareProperties.has_value()) {
+        return hwPropsObj;
+    }
+    env->SetFloatField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.left,
+                       touchpadHardwareProperties->left);
+    env->SetFloatField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.top,
+                       touchpadHardwareProperties->top);
+    env->SetFloatField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.right,
+                       touchpadHardwareProperties->right);
+    env->SetFloatField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.bottom,
+                       touchpadHardwareProperties->bottom);
+    env->SetFloatField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.resX,
+                       touchpadHardwareProperties->res_x);
+    env->SetFloatField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.resY,
+                       touchpadHardwareProperties->res_y);
+    env->SetFloatField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.orientationMinimum,
+                       touchpadHardwareProperties->orientation_minimum);
+    env->SetFloatField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.orientationMaximum,
+                       touchpadHardwareProperties->orientation_maximum);
+    env->SetIntField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.maxFingerCount,
+                     touchpadHardwareProperties->max_finger_cnt);
+    env->SetBooleanField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.isButtonPad,
+                         touchpadHardwareProperties->is_button_pad);
+    env->SetBooleanField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.isHapticPad,
+                         touchpadHardwareProperties->is_haptic_pad);
+    env->SetBooleanField(hwPropsObj, gTouchpadHardwarePropertiesOffsets.reportsPressure,
+                         touchpadHardwareProperties->reports_pressure);
+
+    return hwPropsObj;
+}
+
 static jboolean nativeEnableSensor(JNIEnv* env, jobject nativeImplObj, jint deviceId,
                                    jint sensorType, jint samplingPeriodUs,
                                    jint maxBatchReportLatencyUs) {
@@ -2831,6 +2888,9 @@
         {"setKeyRepeatConfiguration", "(II)V", (void*)nativeSetKeyRepeatConfiguration},
         {"getSensorList", "(I)[Landroid/hardware/input/InputSensorInfo;",
          (void*)nativeGetSensorList},
+        {"getTouchpadHardwareProperties",
+         "(I)Lcom/android/server/input/TouchpadHardwareProperties;",
+         (void*)nativeGetTouchpadHardwareProperties},
         {"enableSensor", "(IIII)Z", (void*)nativeEnableSensor},
         {"disableSensor", "(II)V", (void*)nativeDisableSensor},
         {"flushSensor", "(II)Z", (void*)nativeFlushSensor},
@@ -3090,6 +3150,42 @@
 
     GET_METHOD_ID(gInputSensorInfo.init, gInputSensorInfo.clazz, "<init>", "()V");
 
+    // TouchpadHardawreProperties
+    FIND_CLASS(gTouchpadHardwarePropertiesOffsets.clazz,
+               "com/android/server/input/TouchpadHardwareProperties");
+    gTouchpadHardwarePropertiesOffsets.clazz =
+            reinterpret_cast<jclass>(env->NewGlobalRef(gTouchpadHardwarePropertiesOffsets.clazz));
+
+    // Get the constructor ID
+    GET_METHOD_ID(gTouchpadHardwarePropertiesOffsets.constructor,
+                  gTouchpadHardwarePropertiesOffsets.clazz, "<init>", "()V");
+
+    // Get the field IDs
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.left, gTouchpadHardwarePropertiesOffsets.clazz,
+                 "mLeft", "F");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.top, gTouchpadHardwarePropertiesOffsets.clazz,
+                 "mTop", "F");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.right, gTouchpadHardwarePropertiesOffsets.clazz,
+                 "mRight", "F");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.bottom,
+                 gTouchpadHardwarePropertiesOffsets.clazz, "mBottom", "F");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.resX, gTouchpadHardwarePropertiesOffsets.clazz,
+                 "mResX", "F");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.resY, gTouchpadHardwarePropertiesOffsets.clazz,
+                 "mResY", "F");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.orientationMinimum,
+                 gTouchpadHardwarePropertiesOffsets.clazz, "mOrientationMinimum", "F");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.orientationMaximum,
+                 gTouchpadHardwarePropertiesOffsets.clazz, "mOrientationMaximum", "F");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.maxFingerCount,
+                 gTouchpadHardwarePropertiesOffsets.clazz, "mMaxFingerCount", "S");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.isButtonPad,
+                 gTouchpadHardwarePropertiesOffsets.clazz, "mIsButtonPad", "Z");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.isHapticPad,
+                 gTouchpadHardwarePropertiesOffsets.clazz, "mIsHapticPad", "Z");
+    GET_FIELD_ID(gTouchpadHardwarePropertiesOffsets.reportsPressure,
+                 gTouchpadHardwarePropertiesOffsets.clazz, "mReportsPressure", "Z");
+
     return 0;
 }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index 62400eb..ab0f0c1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -1557,6 +1557,19 @@
         when(ddcMock.getIdleScreenRefreshRateTimeoutLuxThresholdPoint())
                 .thenReturn(List.of(getIdleScreenRefreshRateTimeoutLuxThresholdPoint(6, 1000),
                         getIdleScreenRefreshRateTimeoutLuxThresholdPoint(100, 800)));
+        director.defaultDisplayDeviceUpdated(ddcMock); // set the updated ddc
+
+        // idleScreenRefreshRate config is still null because the flag to enable subscription to
+        // light sensor is not enabled
+        sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 4));
+        waitForIdleSync();
+        assertNull(director.getBrightnessObserver().getIdleScreenRefreshRateConfig());
+
+        // Flag to subscribe to light sensor is enabled, and the sensor subscription is attempted
+        // again to load the idle screen refresh rate config
+        when(mDisplayManagerFlags.isIdleScreenConfigInSubscribingLightSensorEnabled())
+                .thenReturn(true);
+        director.defaultDisplayDeviceUpdated(ddcMock); // set the updated ddc
 
         // Sensor reads 5 lux
         sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 5));
@@ -1575,7 +1588,6 @@
         waitForIdleSync();
         assertEquals(new SurfaceControl.IdleScreenRefreshRateConfig(800),
                 director.getBrightnessObserver().getIdleScreenRefreshRateConfig());
-
     }
 
     @Test
@@ -3231,6 +3243,8 @@
 
     @Test
     public void testNotifyDefaultDisplayDeviceUpdated() {
+        when(mDisplayManagerFlags.isIdleScreenConfigInSubscribingLightSensorEnabled())
+                .thenReturn(true);
         when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate))
             .thenReturn(75);
         when(mResources.getInteger(R.integer.config_defaultRefreshRate))
@@ -3289,6 +3303,8 @@
                 new float[]{ BrightnessSynchronizer.brightnessIntToFloat(5) }, /* delta= */ 0);
         assertArrayEquals(director.getBrightnessObserver().getLowAmbientBrightnessThresholds(),
                 new float[]{10}, /* delta= */ 0);
+        assertNull(director.getBrightnessObserver()
+                .getIdleScreenRefreshRateTimeoutLuxThresholdPoints());
 
 
         // Notify that the default display is updated, such that DisplayDeviceConfig has new values
@@ -3300,6 +3316,10 @@
                 /* defaultRefreshRateInHbmSunlight= */ 75,
                 /* lowPowerSupportedModes= */ List.of(),
                 /* lowLightBlockingZoneSupportedModes= */ List.of());
+        List<IdleScreenRefreshRateTimeoutLuxThresholdPoint>
+                idleScreenRefreshRateTimeoutLuxThresholdPoints =
+                List.of(getIdleScreenRefreshRateTimeoutLuxThresholdPoint(0, 1500),
+                        getIdleScreenRefreshRateTimeoutLuxThresholdPoint(50, 1000));
         when(displayDeviceConfig.getRefreshRateData()).thenReturn(refreshRateData);
         when(displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50);
         when(displayDeviceConfig.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55);
@@ -3311,6 +3331,8 @@
                 .thenReturn(new float[]{0.21f});
         when(displayDeviceConfig.getHighAmbientBrightnessThresholds())
                 .thenReturn(new float[]{2100});
+        when(displayDeviceConfig.getIdleScreenRefreshRateTimeoutLuxThresholdPoint())
+                .thenReturn(idleScreenRefreshRateTimeoutLuxThresholdPoints);
         director.defaultDisplayDeviceUpdated(displayDeviceConfig);
 
         // Verify the new values are from the freshly loaded DisplayDeviceConfig.
@@ -3329,6 +3351,9 @@
                 new float[]{30}, /* delta= */ 0);
         assertEquals(director.getHbmObserver().getRefreshRateInHbmHdr(), 65);
         assertEquals(director.getHbmObserver().getRefreshRateInHbmSunlight(), 75);
+        assertEquals(director.getBrightnessObserver()
+                .getIdleScreenRefreshRateTimeoutLuxThresholdPoints(),
+                idleScreenRefreshRateTimeoutLuxThresholdPoints);
 
         // Notify that the default display is updated, such that DeviceConfig has new values
         FakeDeviceConfig config = mInjector.getDeviceConfig();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
index 37d8f2f..c5157b3 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
@@ -206,22 +206,8 @@
                 20,
                 1234L,
                 UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND,
-                1000L,
-                "CustomConsumer1",
-                1650.0f,
-                450.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_BACKGROUND,
-                2000L,
+                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
+                0L,
                 "CustomConsumer1",
                 1650.0f,
                 450.0f,
@@ -236,10 +222,10 @@
                 UID_0,
                 BatteryConsumer.PROCESS_STATE_FOREGROUND,
                 1000L,
-                "CustomConsumer2",
+                "CustomConsumer1",
                 1650.0f,
-                500.0f,
-                800L
+                100.0f,
+                0L
         );
         verify(statsLogger).buildStatsEvent(
                 1000L,
@@ -250,6 +236,20 @@
                 UID_0,
                 BatteryConsumer.PROCESS_STATE_BACKGROUND,
                 2000L,
+                "CustomConsumer1",
+                1650.0f,
+                350.0f,
+                0L
+        );
+        verify(statsLogger).buildStatsEvent(
+                1000L,
+                20000L,
+                10000L,
+                20,
+                1234L,
+                UID_0,
+                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
+                0,
                 "CustomConsumer2",
                 1650.0f,
                 500.0f,
@@ -352,21 +352,12 @@
 
         for (PowerComponentUsage componentProto : consumerProto.powerComponents) {
             final int componentId = componentProto.component;
-            if (componentId < BatteryConsumer.POWER_COMPONENT_COUNT) {
-                assertEquals(message + " for component " + componentId,
-                        convertMahToDc(consumer.getConsumedPower(componentId)),
-                        componentProto.powerDeciCoulombs);
-                assertEquals(message + " for component " + componentId,
-                        consumer.getUsageDurationMillis(componentId),
-                        componentProto.durationMillis);
-            } else {
-                assertEquals(message + " for custom component " + componentId,
-                        convertMahToDc(consumer.getConsumedPowerForCustomComponent(componentId)),
-                        componentProto.powerDeciCoulombs);
-                assertEquals(message + " for custom component " + componentId,
-                        consumer.getUsageDurationForCustomComponentMillis(componentId),
-                        componentProto.durationMillis);
-            }
+            assertEquals(message + " for component " + componentId,
+                    convertMahToDc(consumer.getConsumedPower(componentId)),
+                    componentProto.powerDeciCoulombs);
+            assertEquals(message + " for component " + componentId,
+                    consumer.getUsageDurationMillis(componentId),
+                    componentProto.durationMillis);
         }
 
         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
@@ -506,13 +497,13 @@
                         BatteryConsumer.POWER_COMPONENT_SCREEN, 300)
                 .setConsumedPower(
                         BatteryConsumer.POWER_COMPONENT_CPU, 400)
-                .setConsumedPowerForCustomComponent(
+                .setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 450)
-                .setConsumedPowerForCustomComponent(
+                .setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1, 500)
                 .setUsageDurationMillis(
                         BatteryConsumer.POWER_COMPONENT_CPU, 600)
-                .setUsageDurationForCustomComponentMillis(
+                .setUsageDurationMillis(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1, 800);
 
         final BatteryConsumer.Key keyFg = uidBuilder.getKey(BatteryConsumer.POWER_COMPONENT_CPU,
@@ -533,6 +524,17 @@
                 .setConsumedPower(keyCached, 9400, BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION)
                 .setUsageDurationMillis(keyFgs, 8400);
 
+        final BatteryConsumer.Key keyCustomFg = uidBuilder.getKey(
+                BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND);
+        final BatteryConsumer.Key keyCustomBg = uidBuilder.getKey(
+                BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND);
+        uidBuilder.setConsumedPower(
+                keyCustomFg, 100, BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
+        uidBuilder.setConsumedPower(
+                keyCustomBg, 350, BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
+
         builder.getOrCreateUidBatteryConsumerBuilder(UID_1)
                 .setPackageWithHighestDrain("myPackage1")
                 .setTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_FOREGROUND, 1234);
@@ -554,11 +556,11 @@
                 .setConsumedPower(
                         BatteryConsumer.POWER_COMPONENT_CAMERA, 20150,
                         BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION)
-                .setConsumedPowerForCustomComponent(
+                .setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 20200)
                 .setUsageDurationMillis(
                         BatteryConsumer.POWER_COMPONENT_CPU, 20300)
-                .setUsageDurationForCustomComponentMillis(
+                .setUsageDurationMillis(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 20400);
 
         // Not used; just to make sure extraneous data doesn't mess things up.
@@ -567,7 +569,7 @@
                 .setConsumedPower(
                         BatteryConsumer.POWER_COMPONENT_CPU, 10100,
                         BatteryConsumer.POWER_MODEL_POWER_PROFILE)
-                .setConsumedPowerForCustomComponent(
+                .setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10200);
 
         return builder.build();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 374426a..17c7efa 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -532,15 +532,15 @@
                     BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
             assertThat(device.getCustomPowerComponentName(componentId0)).isEqualTo("FOO");
             assertThat(device.getCustomPowerComponentName(componentId1)).isEqualTo("BAR");
-            assertThat(device.getConsumedPowerForCustomComponent(componentId0))
+            assertThat(device.getConsumedPower(componentId0))
                     .isWithin(PRECISION).of(27.77777);
-            assertThat(device.getConsumedPowerForCustomComponent(componentId1))
+            assertThat(device.getConsumedPower(componentId1))
                     .isWithin(PRECISION).of(55.55555);
 
             UidBatteryConsumer uid = stats.getUidBatteryConsumers().get(0);
-            assertThat(uid.getConsumedPowerForCustomComponent(componentId0))
+            assertThat(uid.getConsumedPower(componentId0))
                     .isWithin(PRECISION).of(8.33333);
-            assertThat(uid.getConsumedPowerForCustomComponent(componentId1))
+            assertThat(uid.getConsumedPower(componentId1))
                     .isWithin(PRECISION).of(8.33333);
             return null;
         }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
index 52bb5e8..3ae4c32 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
@@ -173,14 +173,15 @@
         assertThat(dump).containsMatch(quote("(not on battery, screen off/doze)") + "\\s*"
                 + "cpu: 123 apps: 123 duration: 456ms");
         assertThat(dump).containsMatch(
-                "UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123\\s*"
-                        + quote("screen=300 cpu=5787 (27s 99ms) cpu:fg=1777 (7s 771ms) "
+                "UID 271: 1200 fg: 1777 bg: 2388 fgs: 1999 cached: 123\\s*"
+                        + quote("screen=300 cpu=400 (600ms) cpu:fg=1777 (7s 771ms) "
                         + "cpu:bg=1888 (8s 881ms) cpu:fgs=1999 (9s 991ms) "
-                        + "cpu:cached=123 (456ms) FOO=500") + "\\s*"
+                        + "cpu:cached=123 (456ms) FOO=500 (800ms) FOO:bg=500 (800ms)") + "\\s*"
                         + quote("(on battery, screen on)") + "\\s*"
-                        + quote("cpu:fg=1777 (7s 771ms)"));
+                        + quote("cpu=1777 (7s 771ms) cpu:fg=1777 (7s 771ms) "
+                        + "FOO=500 (800ms) FOO:bg=500 (800ms)"));
         assertThat(dump).containsMatch("User 42: 30.0\\s*"
-                + quote("cpu=10.0 (30ms) FOO=20.0"));
+                + quote("cpu=10.0 (30ms) FOO=20.0 (40ms)"));
     }
 
     @Test
@@ -198,10 +199,10 @@
         assertThat(dump).contains("cpu: 20100 apps: 10100 duration: 20s 300ms");
         assertThat(dump).contains("FOO: 20200 apps: 10200 duration: 20s 400ms");
         assertThat(dump).containsMatch(
-                "UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123\\s*"
-                        + quote("screen=300 cpu=5787 (600ms) cpu:fg=1777 (7s 771ms) "
+                "UID 271: 1200 fg: 1777 bg: 2388 fgs: 1999 cached: 123\\s*"
+                        + quote("screen=300 cpu=400 (600ms) cpu:fg=1777 (7s 771ms) "
                         + "cpu:bg=1888 (8s 881ms) cpu:fgs=1999 (9s 991ms) "
-                        + "cpu:cached=123 (456ms) FOO=500"));
+                        + "cpu:cached=123 (456ms) FOO=500 (800ms) FOO:bg=500 (800ms)"));
         assertThat(dump).containsMatch("User 42: 30.0\\s*"
                 + quote("cpu=10.0 (30ms) FOO=20.0"));
     }
@@ -225,25 +226,24 @@
                         .add(stats1)
                         .add(stats2)
                         .build();
-
         assertBatteryUsageStats(sum, 42345, 50, 2234, 4345, 1234, 1000, 5000, 5000);
 
         final List<UidBatteryConsumer> uidBatteryConsumers =
                 sum.getUidBatteryConsumers();
         for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) {
             if (uidBatteryConsumer.getUid() == APP_UID1) {
-                assertUidBatteryConsumer(uidBatteryConsumer, 2124, null,
-                        5321, 6900, 532, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 11772,
+                assertUidBatteryConsumer(uidBatteryConsumer, 1200 + 924, null,
+                        5321, 6900, 532, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 400 + 345,
                         POWER_MODEL_UNDEFINED,
-                        956, 1167, 1478,
-                        true, 3554, 3776, 3998, 444, 3554, 15542, 3776, 17762, 3998, 19982,
+                        500 + 456, 1167, 1478,
+                        true, 3554, 4732, 3998, 444, 3554, 15542, 3776, 17762, 3998, 19982,
                         444, 1110);
             } else if (uidBatteryConsumer.getUid() == APP_UID2) {
                 assertUidBatteryConsumer(uidBatteryConsumer, 1332, "bar",
-                        1111, 2220, 2, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 5985,
+                        1111, 2220, 2, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 444,
                         BatteryConsumer.POWER_MODEL_POWER_PROFILE,
                         555, 666, 777,
-                        true, 1777, 1888, 1999, 321, 1777, 7771, 1888, 8881, 1999, 9991,
+                        true, 1777, 2443, 1999, 321, 1777, 7771, 1888, 8881, 1999, 9991,
                         321, 654);
             } else {
                 fail("Unexpected UID " + uidBatteryConsumer.getUid());
@@ -291,9 +291,6 @@
         TypedXmlPullParser parser = Xml.newBinaryPullParser();
         parser.setInput(in, StandardCharsets.UTF_8.name());
         final BatteryUsageStats fromXml = BatteryUsageStats.createFromXml(parser);
-
-        System.out.println("stats = " + stats);
-        System.out.println("fromXml = " + fromXml);
         assertBatteryUsageStats1(fromXml, true);
     }
 
@@ -336,11 +333,11 @@
             builder.getOrCreateUserBatteryConsumerBuilder(USER_ID)
                     .setConsumedPower(
                             BatteryConsumer.POWER_COMPONENT_CPU, 10)
-                    .setConsumedPowerForCustomComponent(
+                    .setConsumedPower(
                             BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 20)
                     .setUsageDurationMillis(
                             BatteryConsumer.POWER_COMPONENT_CPU, 30)
-                    .setUsageDurationForCustomComponentMillis(
+                    .setUsageDurationMillis(
                             BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 40);
         }
         return builder;
@@ -405,11 +402,11 @@
                         BatteryConsumer.POWER_COMPONENT_SCREEN, screenPower, screenPowerModel)
                 .setConsumedPower(
                         BatteryConsumer.POWER_COMPONENT_CPU, cpuPower, cpuPowerModel)
-                .setConsumedPowerForCustomComponent(
+                .setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentPower)
                 .setUsageDurationMillis(
                         BatteryConsumer.POWER_COMPONENT_CPU, cpuDuration)
-                .setUsageDurationForCustomComponentMillis(
+                .setUsageDurationMillis(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentDuration);
         if (builder.isProcessStateDataNeeded()) {
             final BatteryConsumer.Key cpuFgKey = builder.isScreenStateDataNeeded()
@@ -430,6 +427,15 @@
             final BatteryConsumer.Key cachedKey = uidBuilder.getKey(
                     BatteryConsumer.POWER_COMPONENT_CPU,
                     BatteryConsumer.PROCESS_STATE_CACHED);
+            final BatteryConsumer.Key customBgKey = builder.isScreenStateDataNeeded()
+                    ? uidBuilder.getKey(
+                            BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
+                            BatteryConsumer.PROCESS_STATE_BACKGROUND,
+                            BatteryConsumer.SCREEN_STATE_ON,
+                            BatteryConsumer.POWER_STATE_BATTERY)
+                    : uidBuilder.getKey(
+                            BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
+                            BatteryConsumer.PROCESS_STATE_BACKGROUND);
             uidBuilder
                     .setConsumedPower(cpuFgKey, cpuPowerForeground,
                             BatteryConsumer.POWER_MODEL_POWER_PROFILE)
@@ -442,7 +448,10 @@
                     .setUsageDurationMillis(cpuFgsKey, cpuDurationFgs)
                     .setConsumedPower(cachedKey, cpuPowerCached,
                             BatteryConsumer.POWER_MODEL_POWER_PROFILE)
-                    .setUsageDurationMillis(cachedKey, cpuDurationCached);
+                    .setUsageDurationMillis(cachedKey, cpuDurationCached)
+                    .setConsumedPower(customBgKey, customComponentPower,
+                            BatteryConsumer.POWER_MODEL_UNDEFINED)
+                    .setUsageDurationMillis(customBgKey, customComponentDuration);
         }
     }
 
@@ -456,12 +465,12 @@
                         .setConsumedPower(consumedPower)
                         .setConsumedPower(
                                 BatteryConsumer.POWER_COMPONENT_CPU, cpuPower)
-                        .setConsumedPowerForCustomComponent(
+                        .setConsumedPower(
                                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
                                 customComponentPower)
                         .setUsageDurationMillis(
                                 BatteryConsumer.POWER_COMPONENT_CPU, cpuDuration)
-                        .setUsageDurationForCustomComponentMillis(
+                        .setUsageDurationMillis(
                                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
                                 customComponentDuration);
         if (builder.isPowerStateDataNeeded() || builder.isScreenStateDataNeeded()) {
@@ -511,10 +520,10 @@
         for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) {
             if (uidBatteryConsumer.getUid() == APP_UID1) {
                 assertUidBatteryConsumer(uidBatteryConsumer, 1200, "foo",
-                        1000, 1500, 500, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 5787,
+                        1000, 1500, 500, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 400,
                         BatteryConsumer.POWER_MODEL_POWER_PROFILE,
                         500, 600, 800,
-                        true, 1777, 1888, 1999, 123, 1777, 7771, 1888, 8881, 1999, 9991, 123, 456);
+                        true, 1777, 2388, 1999, 123, 1777, 7771, 1888, 8881, 1999, 9991, 123, 456);
             } else {
                 fail("Unexpected UID " + uidBatteryConsumer.getUid());
             }
@@ -593,11 +602,11 @@
                 BatteryConsumer.POWER_COMPONENT_CPU)).isEqualTo(cpuPower);
         assertThat(uidBatteryConsumer.getPowerModel(
                 BatteryConsumer.POWER_COMPONENT_CPU)).isEqualTo(cpuPowerModel);
-        assertThat(uidBatteryConsumer.getConsumedPowerForCustomComponent(
+        assertThat(uidBatteryConsumer.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID)).isEqualTo(customComponentPower);
         assertThat(uidBatteryConsumer.getUsageDurationMillis(
                 BatteryConsumer.POWER_COMPONENT_CPU)).isEqualTo(cpuDuration);
-        assertThat(uidBatteryConsumer.getUsageDurationForCustomComponentMillis(
+        assertThat(uidBatteryConsumer.getUsageDurationMillis(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID)).isEqualTo(
                 customComponentDuration);
         assertThat(uidBatteryConsumer.getCustomPowerComponentCount()).isEqualTo(1);
@@ -678,11 +687,11 @@
             int cpuDuration, int customComponentDuration) {
         assertThat(userBatteryConsumer.getConsumedPower(
                 BatteryConsumer.POWER_COMPONENT_CPU)).isEqualTo(cpuPower);
-        assertThat(userBatteryConsumer.getConsumedPowerForCustomComponent(
+        assertThat(userBatteryConsumer.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID)).isEqualTo(customComponentPower);
         assertThat(userBatteryConsumer.getUsageDurationMillis(
                 BatteryConsumer.POWER_COMPONENT_CPU)).isEqualTo(cpuDuration);
-        assertThat(userBatteryConsumer.getUsageDurationForCustomComponentMillis(
+        assertThat(userBatteryConsumer.getUsageDurationMillis(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID)).isEqualTo(
                 customComponentDuration);
         assertThat(userBatteryConsumer.getCustomPowerComponentCount()).isEqualTo(1);
@@ -697,11 +706,11 @@
                 aggregateBatteryConsumerScopeAllApps);
         assertThat(appsBatteryConsumer.getConsumedPower(
                 BatteryConsumer.POWER_COMPONENT_CPU)).isEqualTo(cpuPower);
-        assertThat(appsBatteryConsumer.getConsumedPowerForCustomComponent(
+        assertThat(appsBatteryConsumer.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID)).isEqualTo(customComponentPower);
         assertThat(appsBatteryConsumer.getUsageDurationMillis(
                 BatteryConsumer.POWER_COMPONENT_CPU)).isEqualTo(cpuDuration);
-        assertThat(appsBatteryConsumer.getUsageDurationForCustomComponentMillis(
+        assertThat(appsBatteryConsumer.getUsageDurationMillis(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID)).isEqualTo(
                 customComponentDuration);
         assertThat(appsBatteryConsumer.getCustomPowerComponentCount()).isEqualTo(1);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
index 4ab706e..5636242 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
@@ -68,26 +68,26 @@
         mStatsRule.apply(calculator);
 
         UidBatteryConsumer uid = mStatsRule.getUidBatteryConsumer(APP_UID);
-        assertThat(uid.getConsumedPowerForCustomComponent(
+        assertThat(uid.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID))
                 .isWithin(PRECISION).of(8.333333);
-        assertThat(uid.getConsumedPowerForCustomComponent(
+        assertThat(uid.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1))
                 .isWithin(PRECISION).of(33.33333);
 
         final BatteryConsumer deviceBatteryConsumer = mStatsRule.getDeviceBatteryConsumer();
-        assertThat(deviceBatteryConsumer.getConsumedPowerForCustomComponent(
+        assertThat(deviceBatteryConsumer.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID))
                 .isWithin(PRECISION).of(27.77777);
-        assertThat(deviceBatteryConsumer.getConsumedPowerForCustomComponent(
+        assertThat(deviceBatteryConsumer.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1))
                 .isWithin(PRECISION).of(55.55555);
 
         final BatteryConsumer appsBatteryConsumer = mStatsRule.getDeviceBatteryConsumer();
-        assertThat(appsBatteryConsumer.getConsumedPowerForCustomComponent(
+        assertThat(appsBatteryConsumer.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID))
                 .isWithin(PRECISION).of(27.77777);
-        assertThat(appsBatteryConsumer.getConsumedPowerForCustomComponent(
+        assertThat(appsBatteryConsumer.getConsumedPower(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1))
                 .isWithin(PRECISION).of(55.55555);
     }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
index 7f7967b..1f5fba6d 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
@@ -21,6 +21,7 @@
 
 import static org.mockito.Mockito.mock;
 
+import android.annotation.NonNull;
 import android.os.AggregateBatteryConsumer;
 import android.os.BatteryConsumer;
 import android.os.BatteryStats;
@@ -129,6 +130,218 @@
     }
 
     @Test
+    public void breakdownByState_processScreenAndPower() throws Exception {
+        BatteryUsageStats actual = prepareBatteryUsageStats(true, true, true);
+        String message = "Actual BatteryUsageStats: " + actual;
+
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                87600000);
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                54321);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_ANY,
+                BatteryConsumer.PROCESS_STATE_ANY, 54321);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_ANY, 54321);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND, 50020);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND,
+                4301);        // Includes "unspecified" proc state
+
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_ANY, BatteryConsumer.SCREEN_STATE_ON,
+                BatteryConsumer.POWER_STATE_BATTERY, 321);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND, BatteryConsumer.SCREEN_STATE_ON,
+                BatteryConsumer.POWER_STATE_BATTERY, 20);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND, BatteryConsumer.SCREEN_STATE_ON,
+                BatteryConsumer.POWER_STATE_BATTERY, 301);  // bg + unsp
+
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_ANY, BatteryConsumer.SCREEN_STATE_OTHER,
+                BatteryConsumer.POWER_STATE_BATTERY, 4000);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND, BatteryConsumer.SCREEN_STATE_OTHER,
+                BatteryConsumer.POWER_STATE_BATTERY, 4000);
+
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_ANY, BatteryConsumer.SCREEN_STATE_OTHER,
+                BatteryConsumer.POWER_STATE_OTHER, 50000);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND, BatteryConsumer.SCREEN_STATE_OTHER,
+                BatteryConsumer.POWER_STATE_OTHER, 50000);
+
+        actual.close();
+    }
+
+    @Test
+    public void breakdownByState_processAndScreen() throws Exception {
+        BatteryUsageStats actual = prepareBatteryUsageStats(true, true, false);
+        String message = "Actual BatteryUsageStats: " + actual;
+
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                7600000);       // off-battery not included
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.SCREEN_STATE_ON, BatteryConsumer.POWER_STATE_ANY,
+                600000);
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.SCREEN_STATE_OTHER, BatteryConsumer.POWER_STATE_ANY,
+                7000000);
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                4321);       // off-battery not included
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.SCREEN_STATE_ON, BatteryConsumer.POWER_STATE_ANY,
+                321);
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.SCREEN_STATE_OTHER, BatteryConsumer.POWER_STATE_ANY,
+                4000);
+
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_ANY,
+                BatteryConsumer.PROCESS_STATE_ANY, 4321);      // off-battery not included
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_ANY, 4321);      // off-battery not included
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND, 20); // off-battery not included
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND,
+                4301);    // includes unspecified proc state
+
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_ANY, BatteryConsumer.SCREEN_STATE_ON,
+                BatteryConsumer.POWER_STATE_ANY, 321);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND, BatteryConsumer.SCREEN_STATE_ON,
+                BatteryConsumer.POWER_STATE_ANY, 20);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND, BatteryConsumer.SCREEN_STATE_ON,
+                BatteryConsumer.POWER_STATE_ANY, 301);
+
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_ANY, BatteryConsumer.SCREEN_STATE_OTHER,
+                BatteryConsumer.POWER_STATE_ANY, 4000);
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND, BatteryConsumer.SCREEN_STATE_OTHER,
+                BatteryConsumer.POWER_STATE_ANY, 4000);
+
+        actual.close();
+    }
+
+    @Test
+    public void breakdownByState_processStateOnly() throws Exception {
+        BatteryUsageStats actual = prepareBatteryUsageStats(true, false, false);
+        String message = "Actual BatteryUsageStats: " + actual;
+
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                7600000);        // off-battery not included
+        assertAggregatedPowerEstimate(message, actual,
+                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS,
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                4321);      // off-battery not included
+
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_ANY,
+                BatteryConsumer.PROCESS_STATE_ANY, 4321);           // off-battery not included
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_ANY, 4321);           // off-battery not included
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND, 20);      // off-battery not included
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND,
+                4301);    // includes unspecified proc state
+
+        actual.close();
+    }
+
+    private @NonNull BatteryUsageStats prepareBatteryUsageStats(boolean includeProcessStateData,
+            boolean includeScreenStateData, boolean includesPowerStateData) {
+        long[] deviceStats = new long[mCpuStatsArrayLayout.getDeviceStatsArrayLength()];
+        long[] uidStats = new long[mCpuStatsArrayLayout.getUidStatsArrayLength()];
+
+        AggregatedPowerStats aps = new AggregatedPowerStats(mPowerStatsAggregator.getConfig());
+        PowerComponentAggregatedPowerStats stats = aps.getPowerComponentStats(
+                BatteryConsumer.POWER_COMPONENT_CPU);
+        stats.setPowerStatsDescriptor(mPowerStatsDescriptor);
+
+        mCpuStatsArrayLayout.setUidPowerEstimate(uidStats, 1);
+        stats.setUidStats(APP_UID1, new int[]{
+                AggregatedPowerStatsConfig.POWER_STATE_BATTERY,
+                AggregatedPowerStatsConfig.SCREEN_STATE_ON,
+                BatteryConsumer.PROCESS_STATE_UNSPECIFIED}, uidStats);
+
+        mCpuStatsArrayLayout.setUidPowerEstimate(uidStats, 20);
+        stats.setUidStats(APP_UID1, new int[]{
+                AggregatedPowerStatsConfig.POWER_STATE_BATTERY,
+                AggregatedPowerStatsConfig.SCREEN_STATE_ON,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND}, uidStats);
+
+        mCpuStatsArrayLayout.setUidPowerEstimate(uidStats, 300);
+        stats.setUidStats(APP_UID1, new int[]{
+                AggregatedPowerStatsConfig.POWER_STATE_BATTERY,
+                AggregatedPowerStatsConfig.SCREEN_STATE_ON,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND}, uidStats);
+
+        mCpuStatsArrayLayout.setUidPowerEstimate(uidStats, 4000);
+        stats.setUidStats(APP_UID1, new int[]{
+                AggregatedPowerStatsConfig.POWER_STATE_BATTERY,
+                AggregatedPowerStatsConfig.SCREEN_STATE_OTHER,
+                BatteryConsumer.PROCESS_STATE_BACKGROUND}, uidStats);
+
+        mCpuStatsArrayLayout.setUidPowerEstimate(uidStats, 50000);
+        stats.setUidStats(APP_UID1, new int[]{
+                AggregatedPowerStatsConfig.POWER_STATE_OTHER,
+                AggregatedPowerStatsConfig.SCREEN_STATE_OTHER,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND}, uidStats);
+
+        mCpuStatsArrayLayout.setDevicePowerEstimate(deviceStats, 600000);
+        stats.setDeviceStats(new int[]{
+                AggregatedPowerStatsConfig.POWER_STATE_BATTERY,
+                AggregatedPowerStatsConfig.SCREEN_STATE_ON}, deviceStats);
+
+        mCpuStatsArrayLayout.setDevicePowerEstimate(deviceStats, 7000000);
+        stats.setDeviceStats(new int[]{
+                AggregatedPowerStatsConfig.POWER_STATE_BATTERY,
+                AggregatedPowerStatsConfig.SCREEN_STATE_OTHER}, deviceStats);
+
+        mCpuStatsArrayLayout.setDevicePowerEstimate(deviceStats, 80000000);
+        stats.setDeviceStats(new int[]{
+                AggregatedPowerStatsConfig.POWER_STATE_OTHER,
+                AggregatedPowerStatsConfig.SCREEN_STATE_ON}, deviceStats);
+
+        return exportToBatteryUsageStats(aps, includeProcessStateData,
+                includeScreenStateData, includesPowerStateData);
+    }
+
+    private @NonNull BatteryUsageStats exportToBatteryUsageStats(AggregatedPowerStats aps,
+            boolean includeProcessStateData, boolean includeScreenStateData,
+            boolean includesPowerStateData) {
+        PowerStatsExporter exporter = new PowerStatsExporter(mPowerStatsStore,
+                mPowerStatsAggregator, /* batterySessionTimeSpanSlackMillis */ 0);
+
+        BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(new String[0], false,
+                includeProcessStateData, includeScreenStateData, includesPowerStateData, 0);
+        exporter.populateBatteryUsageStatsBuilder(builder, aps);
+        return builder.build();
+    }
+
+    @Test
     public void breakdownByProcState_fullRange() throws Exception {
         breakdownByProcState_fullRange(false, false);
     }
@@ -232,19 +445,28 @@
                 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS,
                 BatteryConsumer.POWER_COMPONENT_CPU, 7.51016);
 
+        assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_ANY,
+                BatteryConsumer.PROCESS_STATE_ANY, 4.33);
         assertUidPowerEstimate(message, actual, APP_UID1, BatteryConsumer.POWER_COMPONENT_CPU,
                 BatteryConsumer.PROCESS_STATE_ANY, 3.97099);
+        assertUidPowerEstimate(message, actual, APP_UID1,
+                BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
+                BatteryConsumer.PROCESS_STATE_ANY, 0.360);
+
+        assertUidPowerEstimate(message, actual, APP_UID2, BatteryConsumer.POWER_COMPONENT_ANY,
+                BatteryConsumer.PROCESS_STATE_ANY, 3.538999);
         assertUidPowerEstimate(message, actual, APP_UID2, BatteryConsumer.POWER_COMPONENT_CPU,
                 BatteryConsumer.PROCESS_STATE_ANY, 3.538999);
         UidBatteryConsumer uidScope = actual.getUidBatteryConsumers().stream()
                 .filter(us -> us.getUid() == APP_UID1).findFirst().orElse(null);
         // There shouldn't be any per-procstate data
         for (int procState = 0; procState < BatteryConsumer.PROCESS_STATE_COUNT; procState++) {
-            if (procState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
-                assertThat(uidScope.getConsumedPower(new BatteryConsumer.Dimensions(
-                        BatteryConsumer.POWER_COMPONENT_CPU,
-                        BatteryConsumer.PROCESS_STATE_FOREGROUND))).isEqualTo(0);
+            if (procState == BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+                continue;
             }
+            double power = uidScope.getConsumedPower(
+                    new BatteryConsumer.Dimensions(BatteryConsumer.POWER_COMPONENT_CPU, procState));
+            assertWithMessage("procState=" + procState).that(power).isEqualTo(0);
         }
         actual.close();
     }
@@ -333,22 +555,34 @@
     private void assertAggregatedPowerEstimate(String message, BatteryUsageStats bus, int scope,
             int componentId, double expected) {
         AggregateBatteryConsumer consumer = bus.getAggregateBatteryConsumer(scope);
-        double actual = componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                ? consumer.getConsumedPower(componentId)
-                : consumer.getConsumedPowerForCustomComponent(componentId);
+        double actual = consumer.getConsumedPower(componentId);
+        assertWithMessage(message).that(actual).isWithin(TOLERANCE).of(expected);
+    }
+
+    private void assertAggregatedPowerEstimate(String message, BatteryUsageStats bus, int scope,
+            int componentId, int screenState, int powerState, double expected) {
+        AggregateBatteryConsumer consumer = bus.getAggregateBatteryConsumer(scope);
+        double actual = consumer.getConsumedPower(
+                new BatteryConsumer.Dimensions(componentId, BatteryConsumer.PROCESS_STATE_ANY,
+                        screenState, powerState));
         assertWithMessage(message).that(actual).isWithin(TOLERANCE).of(expected);
     }
 
     private void assertUidPowerEstimate(String message, BatteryUsageStats bus, int uid,
             int componentId, int processState, double expected) {
+        assertUidPowerEstimate(message, bus, uid, componentId, processState,
+                BatteryConsumer.SCREEN_STATE_ANY, BatteryConsumer.POWER_STATE_ANY,
+                expected);
+    }
+
+    private void assertUidPowerEstimate(String message, BatteryUsageStats bus, int uid,
+            int componentId, int processState, int screenState, int powerState, double expected) {
         List<UidBatteryConsumer> uidScopes = bus.getUidBatteryConsumers();
         final UidBatteryConsumer uidScope = uidScopes.stream()
                 .filter(us -> us.getUid() == uid).findFirst().orElse(null);
         assertWithMessage(message).that(uidScope).isNotNull();
-        double actual = componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                ? uidScope.getConsumedPower(
-                        new BatteryConsumer.Dimensions(componentId, processState))
-                : uidScope.getConsumedPowerForCustomComponent(componentId);
+        double actual = uidScope.getConsumedPower(
+                new BatteryConsumer.Dimensions(componentId, processState, screenState, powerState));
         assertWithMessage(message).that(actual).isWithin(TOLERANCE).of(expected);
     }
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerStatsProcessorTest.java
index 9fde61a..c05a910 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerStatsProcessorTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server.power.stats;
 
-import static android.os.BatteryConsumer.PROCESS_STATE_ANY;
-
 import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_BATTERY;
 import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_OTHER;
 import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_ON;
@@ -280,7 +278,7 @@
         ScreenPowerStatsLayout layout = new ScreenPowerStatsLayout(descriptor);
         long[] stats = new long[descriptor.uidStatsArrayLength];
         aggregatedStats.getUidStats(stats, uid,
-                new int[]{powerState, screenState, PROCESS_STATE_ANY});
+                new int[]{powerState, screenState, BatteryConsumer.PROCESS_STATE_UNSPECIFIED});
         assertThat(layout.getUidPowerEstimate(stats)).isWithin(PRECISION)
                 .of(expectedScreenPowerEstimate);
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index 6a99731..411a610 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -89,7 +89,7 @@
 import javax.annotation.Nullable;
 
 @RunWith(AndroidJUnit4.class)
-@EnableFlags(Flags.FLAG_VISIT_PERSON_URI)
+@EnableFlags({Flags.FLAG_VISIT_PERSON_URI, Flags.FLAG_API_RICH_ONGOING})
 public class NotificationVisitUrisTest extends UiServiceTestCase {
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
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 ed8ebc8..dd2b845 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -82,6 +82,7 @@
 import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL;
 import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -6672,6 +6673,91 @@
         assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
     }
 
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void setAutomaticZenRuleState_withActivationOverride_userActionFromAppCanDeactivate() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+                .setPackage(mPkg)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+                CUSTOM_PKG_UID);
+
+        // User manually turns on rule from SysUI / Settings...
+        mZenModeHelper.setAutomaticZenRuleState(ruleId,
+                new Condition(rule.getConditionId(), "manual-on-from-sysui", STATE_TRUE,
+                        SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+        assertThat(getZenRule(ruleId).isAutomaticActive()).isTrue();
+        assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE);
+
+        // ... and they can turn it off manually from inside the app.
+        mZenModeHelper.setAutomaticZenRuleState(ruleId,
+                new Condition(rule.getConditionId(), "manual-off-from-app", STATE_FALSE,
+                        SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID);
+        assertThat(getZenRule(ruleId).isAutomaticActive()).isFalse();
+        assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void setAutomaticZenRuleState_withDeactivationOverride_userActionFromAppCanActivate() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+                .setPackage(mPkg)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+                CUSTOM_PKG_UID);
+
+        // Rule is activated due to its schedule.
+        mZenModeHelper.setAutomaticZenRuleState(ruleId,
+                new Condition(rule.getConditionId(), "auto-on-from-app", STATE_TRUE,
+                        SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID);
+        assertThat(getZenRule(ruleId).isAutomaticActive()).isTrue();
+        assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+
+        // User manually turns off rule from SysUI / Settings...
+        mZenModeHelper.setAutomaticZenRuleState(ruleId,
+                new Condition(rule.getConditionId(), "manual-off-from-sysui", STATE_FALSE,
+                        SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+        assertThat(getZenRule(ruleId).isAutomaticActive()).isFalse();
+        assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE);
+
+        // ... and they can turn it on manually from inside the app.
+        mZenModeHelper.setAutomaticZenRuleState(ruleId,
+                new Condition(rule.getConditionId(), "manual-on-from-app", STATE_TRUE,
+                        SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID);
+        assertThat(getZenRule(ruleId).isAutomaticActive()).isTrue();
+        assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void setAutomaticZenRuleState_manualActionFromApp_isNotOverride() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+                .setPackage(mPkg)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+                CUSTOM_PKG_UID);
+
+        // Rule is manually activated by the user in the app.
+        // This turns the rule on, but is NOT an override...
+        mZenModeHelper.setAutomaticZenRuleState(ruleId,
+                new Condition(rule.getConditionId(), "manual-on-from-app", STATE_TRUE,
+                        SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID);
+        assertThat(getZenRule(ruleId).isAutomaticActive()).isTrue();
+        assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+
+        // ... so the app can turn it off when its schedule is over.
+        mZenModeHelper.setAutomaticZenRuleState(ruleId,
+                new Condition(rule.getConditionId(), "auto-off-from-app", STATE_FALSE,
+                        SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID);
+        assertThat(getZenRule(ruleId).isAutomaticActive()).isFalse();
+        assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+    }
+
+    private ZenRule getZenRule(String ruleId) {
+        return checkNotNull(mZenModeHelper.mConfig.automaticRules.get(ruleId),
+                "Didn't find rule with id %s", ruleId);
+    }
+
     private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
             @Nullable ZenPolicy zenPolicy) {
         ZenRule rule = new ZenRule();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 8d1ba5b..c788f3b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -243,6 +243,18 @@
         doReturn(embedded).when(mActivityStack.top()).isEmbedded();
     }
 
+    void setTopActivityVisible(boolean isVisible) {
+        doReturn(isVisible).when(mActivityStack.top()).isVisible();
+    }
+
+    void setTopActivityVisibleRequested(boolean isVisibleRequested) {
+        doReturn(isVisibleRequested).when(mActivityStack.top()).isVisibleRequested();
+    }
+
+    void setTopActivityFillsParent(boolean fillsParent) {
+        doReturn(fillsParent).when(mActivityStack.top()).fillsParent();
+    }
+
     void setTopActivityInMultiWindowMode(boolean multiWindowMode) {
         doReturn(multiWindowMode).when(mActivityStack.top()).inMultiWindowMode();
         if (multiWindowMode) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
index 7760051..05f6ed6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
@@ -26,6 +26,8 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
+
 /**
  * Robot implementation for {@link AppCompatConfiguration}.
  */
@@ -99,6 +101,32 @@
                 .getThinLetterboxHeightPx();
     }
 
+    void setLetterboxActivityCornersRounded(boolean rounded) {
+        doReturn(rounded).when(mAppCompatConfiguration).isLetterboxActivityCornersRounded();
+    }
+
+    void setLetterboxEducationEnabled(boolean enabled) {
+        doReturn(enabled).when(mAppCompatConfiguration).getIsEducationEnabled();
+    }
+
+    void setLetterboxActivityCornersRadius(int cornerRadius) {
+        doReturn(cornerRadius).when(mAppCompatConfiguration).getLetterboxActivityCornersRadius();
+    }
+
+    void setLetterboxBackgroundType(@LetterboxBackgroundType int backgroundType) {
+        doReturn(backgroundType).when(mAppCompatConfiguration).getLetterboxBackgroundType();
+    }
+
+    void setLetterboxBackgroundWallpaperBlurRadiusPx(int blurRadiusPx) {
+        doReturn(blurRadiusPx).when(mAppCompatConfiguration)
+                .getLetterboxBackgroundWallpaperBlurRadiusPx();
+    }
+
+    void setLetterboxBackgroundWallpaperDarkScrimAlpha(float darkScrimAlpha) {
+        doReturn(darkScrimAlpha).when(mAppCompatConfiguration)
+                .getLetterboxBackgroundWallpaperDarkScrimAlpha();
+    }
+
     void checkToNextLeftStop(boolean invoked) {
         verify(mAppCompatConfiguration, times(invoked ? 1 : 0))
                 .movePositionForHorizontalReachabilityToNextLeftStop(anyBoolean());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxOverrideTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxOverrideTest.java
new file mode 100644
index 0000000..af7f881
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxOverrideTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_OVERRIDE_UNSET;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link AppCompatLetterboxOverrides}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:AppCompatLetterboxOverrideTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatLetterboxOverrideTest extends WindowTestsBase {
+
+    @Test
+    public void testIsLetterboxEducationEnabled() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+
+            robot.conf().setLetterboxEducationEnabled(/* enabled */ true);
+            robot.checkLetterboxEducationEnabled(/* enabled */ true);
+
+            robot.conf().setLetterboxEducationEnabled(/* enabled */ false);
+            robot.checkLetterboxEducationEnabled(/* enabled */ false);
+        });
+    }
+
+    @Test
+    public void testShouldLetterboxHaveRoundedCorners() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.activity().setTopActivityFillsParent(/* fillsParent */ true);
+            robot.checkShouldLetterboxHaveRoundedCorners(/* expected */ true);
+
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ false);
+            robot.checkShouldLetterboxHaveRoundedCorners(/* expected */ false);
+
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.activity().setTopActivityFillsParent(/* fillsParent */ false);
+            robot.checkShouldLetterboxHaveRoundedCorners(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testHasWallpaperBackgroundForLetterbox() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+
+            robot.invokeCheckWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */ false);
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+
+            robot.invokeCheckWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */ true);
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ true);
+
+            robot.invokeCheckWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */ true);
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ true);
+
+            robot.invokeCheckWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */ false);
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testCheckWallpaperBackgroundForLetterbox() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+
+            robot.checkWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */
+                    true, /* expected */ true);
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ true);
+
+            robot.checkWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */
+                    true, /* expected */ false);
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ true);
+
+            robot.checkWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */
+                    false, /* expected */ true);
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+
+            robot.checkWallpaperBackgroundForLetterbox(/* wallpaperShouldBeShown */
+                    false, /* expected */ false);
+            robot.checkHasWallpaperBackgroundForLetterbox(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testLetterboxActivityCornersRadius() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+
+            robot.conf().setLetterboxActivityCornersRadius(/* cornerRadius */ 0);
+            robot.checkLetterboxActivityCornersRadius(/* cornerRadius */ 0);
+
+            robot.conf().setLetterboxActivityCornersRadius(/* cornerRadius */ 37);
+            robot.checkLetterboxActivityCornersRadius(/* cornerRadius */ 37);
+
+            robot.conf().setLetterboxActivityCornersRadius(/* cornerRadius */ 5);
+            robot.checkLetterboxActivityCornersRadius(/* cornerRadius */ 5);
+        });
+    }
+
+    @Test
+    public void testLetterboxActivityCornersRaunded() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.checkLetterboxActivityCornersRounded(/* expected */ true);
+
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ false);
+            robot.checkLetterboxActivityCornersRounded(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testLetterboxBackgroundType() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+
+            robot.conf().setLetterboxBackgroundType(LETTERBOX_BACKGROUND_OVERRIDE_UNSET);
+            robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_OVERRIDE_UNSET);
+
+            robot.conf().setLetterboxBackgroundType(LETTERBOX_BACKGROUND_SOLID_COLOR);
+            robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_SOLID_COLOR);
+
+            robot.conf().setLetterboxBackgroundType(LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND);
+            robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND);
+
+            robot.conf().setLetterboxBackgroundType(
+                    LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING);
+            robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING);
+
+            robot.conf().setLetterboxBackgroundType(LETTERBOX_BACKGROUND_WALLPAPER);
+            robot.checkLetterboxBackgroundType(LETTERBOX_BACKGROUND_WALLPAPER);
+        });
+    }
+
+    @Test
+    public void testLetterboxBackgroundWallpaperBlurRadiusPx() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+
+            robot.conf().setLetterboxBackgroundWallpaperBlurRadiusPx(-1);
+            robot.checkLetterboxWallpaperBlurRadiusPx(0);
+
+            robot.conf().setLetterboxBackgroundWallpaperBlurRadiusPx(0);
+            robot.checkLetterboxWallpaperBlurRadiusPx(0);
+
+            robot.conf().setLetterboxBackgroundWallpaperBlurRadiusPx(10);
+            robot.checkLetterboxWallpaperBlurRadiusPx(10);
+        });
+    }
+
+    @Test
+    public void testLetterboxBackgroundWallpaperDarkScrimAlpha() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+
+            robot.conf().setLetterboxBackgroundWallpaperDarkScrimAlpha(-1f);
+            robot.checkLetterboxWallpaperDarkScrimAlpha(0);
+
+            robot.conf().setLetterboxBackgroundWallpaperDarkScrimAlpha(1.1f);
+            robot.checkLetterboxWallpaperDarkScrimAlpha(0);
+
+            robot.conf().setLetterboxBackgroundWallpaperDarkScrimAlpha(0.001f);
+            robot.checkLetterboxWallpaperDarkScrimAlpha(0.001f);
+
+            robot.conf().setLetterboxBackgroundWallpaperDarkScrimAlpha(0.999f);
+            robot.checkLetterboxWallpaperDarkScrimAlpha(0.999f);
+        });
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    void runTestScenario(@NonNull Consumer<LetterboxOverridesRobotTest> consumer) {
+        final LetterboxOverridesRobotTest robot =
+                new LetterboxOverridesRobotTest(mWm, mAtm, mSupervisor);
+        consumer.accept(robot);
+    }
+
+    private static class LetterboxOverridesRobotTest extends AppCompatRobotBase {
+        LetterboxOverridesRobotTest(@NonNull WindowManagerService wm,
+                @NonNull ActivityTaskManagerService atm,
+                @NonNull ActivityTaskSupervisor supervisor) {
+            super(wm, atm, supervisor);
+        }
+
+        void invokeCheckWallpaperBackgroundForLetterbox(boolean wallpaperShouldBeShown) {
+            getLetterboxOverrides().checkWallpaperBackgroundForLetterbox(wallpaperShouldBeShown);
+        }
+
+        void checkLetterboxEducationEnabled(boolean enabled) {
+            assertEquals(enabled, getLetterboxOverrides().isLetterboxEducationEnabled());
+        }
+
+        void checkShouldLetterboxHaveRoundedCorners(boolean expected) {
+            assertEquals(expected,
+                    getLetterboxOverrides().shouldLetterboxHaveRoundedCorners());
+        }
+
+        void checkHasWallpaperBackgroundForLetterbox(boolean expected) {
+            assertEquals(expected,
+                    getLetterboxOverrides().hasWallpaperBackgroundForLetterbox());
+        }
+
+        void checkWallpaperBackgroundForLetterbox(boolean wallpaperShouldBeShown,
+                boolean expected) {
+            assertEquals(expected,
+                    getLetterboxOverrides().checkWallpaperBackgroundForLetterbox(
+                            wallpaperShouldBeShown));
+        }
+
+        void checkLetterboxActivityCornersRadius(int expected) {
+            assertEquals(expected, getLetterboxOverrides().getLetterboxActivityCornersRadius());
+        }
+
+        void checkLetterboxActivityCornersRounded(boolean expected) {
+            assertEquals(expected, getLetterboxOverrides().isLetterboxActivityCornersRounded());
+        }
+
+        void checkLetterboxBackgroundType(@LetterboxBackgroundType int expected) {
+            assertEquals(expected, getLetterboxOverrides().getLetterboxBackgroundType());
+        }
+
+        void checkLetterboxWallpaperBlurRadiusPx(int expected) {
+            assertEquals(expected, getLetterboxOverrides().getLetterboxWallpaperBlurRadiusPx());
+        }
+
+        void checkLetterboxWallpaperDarkScrimAlpha(float expected) {
+            assertEquals(expected, getLetterboxOverrides().getLetterboxWallpaperDarkScrimAlpha(),
+                    FLOAT_TOLLERANCE);
+        }
+
+        @NonNull
+        private AppCompatLetterboxOverrides getLetterboxOverrides() {
+            return activity().top().mAppCompatController.getAppCompatLetterboxOverrides();
+        }
+
+    }
+
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java
new file mode 100644
index 0000000..e046f7c
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.RoundedCorner;
+import android.view.RoundedCorners;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link AppCompatLetterboxPolicy}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:AppCompatLetterboxPolicyTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatLetterboxPolicyTest extends WindowTestsBase {
+
+    @Test
+    public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() {
+        runTestScenario((robot) -> {
+            robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true);
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+            robot.setTopActivityTransparentPolicyRunning(/* running */ true);
+            robot.activity().configureTopActivityBounds(new Rect(0, 0, 500, 300));
+
+            robot.resizeMainWindow(/* newWidth */ 499, /* newHeight */ 299);
+            robot.checkWindowStateHasCropBounds(/* expected */ false);
+
+            robot.resizeMainWindow(/* newWidth */ 500, /* newHeight */ 300);
+            robot.checkWindowStateHasCropBounds(/* expected */ true);
+        });
+    }
+
+    @Test
+    public void testGetCropBoundsIfNeeded_noCrop() {
+        runTestScenario((robot) -> {
+            robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ false);
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+            // Do not apply crop if taskbar is collapsed
+            robot.collapseTaskBar();
+            robot.checkTaskBarIsExpanded(/* expected */ false);
+
+            robot.activity().configureTopActivityBounds(new Rect(50, 25, 150, 75));
+            robot.checkWindowStateHasCropBounds(/* expected */ true);
+            // Expected the same size of the activity.
+            robot.validateWindowStateCropBounds(0, 0, 100, 50);
+        });
+    }
+
+    @Test
+    public void testGetCropBoundsIfNeeded_appliesCrop() {
+        runTestScenario((robot) -> {
+            robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true);
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+            // Apply crop if taskbar is expanded.
+            robot.expandTaskBar();
+            robot.checkTaskBarIsExpanded(/* expected */ true);
+
+            robot.activity().configureTopActivityBounds(new Rect(50, 0, 150, 100));
+            robot.checkWindowStateHasCropBounds(/* expected */ true);
+            // The task bar expanded height is removed from the crop height.
+            robot.validateWindowStateCropBounds(0, 0, 100, 80);
+        });
+    }
+
+    @Test
+    public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() {
+        runTestScenario((robot) -> {
+            robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true);
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+            // Apply crop if taskbar is expanded.
+            robot.expandTaskBar();
+            robot.checkTaskBarIsExpanded(/* expected */ true);
+            robot.activity().setTopActivityInSizeCompatMode(/* isScm */ true);
+            robot.setInvCompatState(/* scale */ 2.0f);
+
+            robot.activity().configureTopActivityBounds(new Rect(50, 0, 150, 100));
+
+            robot.checkWindowStateHasCropBounds(/* expected */ true);
+            // The width and height, considering task bar, are scaled by 2.
+            robot.validateWindowStateCropBounds(0, 0, 200, 160);
+        });
+    }
+
+    @Test
+    public void testGetRoundedCornersRadius_withRoundedCornersFromInsets() {
+        runTestScenario((robot) -> {
+            robot.conf().setLetterboxActivityCornersRadius(-1);
+            robot.configureWindowState();
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+            robot.setInvCompatState(/* scale */ 0.5f);
+            robot.configureInsetsRoundedCorners(new RoundedCorners(
+                    /*topLeft=*/ null,
+                    /*topRight=*/ null,
+                    /*bottomRight=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT,
+                    /* configurationRadius */ 15, /*centerX=*/ 1, /*centerY=*/ 1),
+                    /*bottomLeft=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT,
+                    30 /*2 is to test selection of the min radius*/,
+                    /*centerX=*/ 1, /*centerY=*/ 1)
+            ));
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 7);
+        });
+    }
+
+
+    @Test
+    public void testGetRoundedCornersRadius_withLetterboxActivityCornersRadius() {
+        runTestScenario((robot) -> {
+            robot.conf().setLetterboxActivityCornersRadius(15);
+            robot.configureWindowState();
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+            robot.setInvCompatState(/* scale */ 0.5f);
+
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ true);
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 7);
+
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 7);
+
+            robot.activity().setTopActivityVisibleRequested(/* isVisibleRequested */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ false);
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 0);
+
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ true);
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 7);
+        });
+    }
+
+    @Test
+    public void testGetRoundedCornersRadius_noScalingApplied() {
+        runTestScenario((robot) -> {
+            robot.conf().setLetterboxActivityCornersRadius(15);
+            robot.configureWindowState();
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false);
+            robot.activity().setTopActivityVisible(/* isVisible */ true);
+            robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true);
+            robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true);
+            robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20);
+
+            robot.setInvCompatState(/* scale */ -1f);
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 15);
+
+            robot.setInvCompatState(/* scale */ 0f);
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 15);
+
+            robot.setInvCompatState(/* scale */ 1f);
+            robot.checkWindowStateRoundedCornersRadius(/* expected */ 15);
+        });
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    void runTestScenario(@NonNull Consumer<LetterboxPolicyRobotTest> consumer) {
+        final LetterboxPolicyRobotTest robot = new LetterboxPolicyRobotTest(mWm, mAtm, mSupervisor);
+        consumer.accept(robot);
+    }
+
+    private static class LetterboxPolicyRobotTest extends AppCompatRobotBase {
+
+        static final int TASKBAR_COLLAPSED_HEIGHT = 10;
+        static final int TASKBAR_EXPANDED_HEIGHT = 20;
+        private static final int SCREEN_WIDTH = 200;
+        private static final int SCREEN_HEIGHT = 100;
+        static final Rect TASKBAR_COLLAPSED_BOUNDS = new Rect(0,
+                SCREEN_HEIGHT - TASKBAR_COLLAPSED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
+        static final Rect TASKBAR_EXPANDED_BOUNDS = new Rect(0,
+                SCREEN_HEIGHT - TASKBAR_EXPANDED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
+
+        @NonNull
+        private final WindowState mWindowState;
+        @Nullable
+        private InsetsSource mTaskbar;
+        @Nullable
+        private InsetsState mInsetsState;
+
+        LetterboxPolicyRobotTest(@NonNull WindowManagerService wm,
+                @NonNull ActivityTaskManagerService atm,
+                @NonNull ActivityTaskSupervisor supervisor) {
+            super(wm, atm, supervisor);
+            mWindowState = mock(WindowState.class);
+        }
+
+        @Override
+        void onPostActivityCreation(@NonNull ActivityRecord activity) {
+            super.onPostActivityCreation(activity);
+            spyOn(getAspectRatioPolicy());
+            spyOn(getTransparentPolicy());
+        }
+
+        void configureWindowStateWithTaskBar(boolean hasInsetsRoundedCorners) {
+            configureWindowState(/* withTaskBar */ true, hasInsetsRoundedCorners);
+        }
+
+        void configureWindowState() {
+            configureWindowState(/* withTaskBar */ false, /* hasInsetsRoundedCorners */ false);
+        }
+
+        void configureInsetsRoundedCorners(@NonNull RoundedCorners roundedCorners) {
+            mInsetsState.setRoundedCorners(roundedCorners);
+        }
+
+        private void configureWindowState(boolean withTaskBar, boolean hasInsetsRoundedCorners) {
+            mInsetsState = new InsetsState();
+            if (withTaskBar) {
+                mTaskbar = new InsetsSource(/*id=*/ 0,
+                        WindowInsets.Type.navigationBars());
+                if (hasInsetsRoundedCorners) {
+                    mTaskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
+                }
+                mTaskbar.setVisible(true);
+                mInsetsState.addSource(mTaskbar);
+            }
+            mWindowState.mInvGlobalScale = 1f;
+            final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+            doReturn(mInsetsState).when(mWindowState).getInsetsState();
+            doReturn(attrs).when(mWindowState).getAttrs();
+            doReturn(true).when(mWindowState).isDrawn();
+            doReturn(true).when(mWindowState).isOnScreen();
+            doReturn(false).when(mWindowState).isLetterboxedForDisplayCutout();
+            doReturn(true).when(mWindowState).areAppWindowBoundsLetterboxed();
+        }
+
+        void setInvCompatState(float scale) {
+            mWindowState.mInvGlobalScale = scale;
+        }
+
+        void setTopActivityInLetterboxAnimation(boolean inLetterboxAnimation) {
+            doReturn(inLetterboxAnimation).when(activity().top()).isInLetterboxAnimation();
+        }
+
+        void setTopActivityTransparentPolicyRunning(boolean running) {
+            doReturn(running).when(getTransparentPolicy()).isRunning();
+        }
+
+        void setIsLetterboxedForFixedOrientationAndAspectRatio(boolean isLetterboxed) {
+            doReturn(isLetterboxed).when(getAspectRatioPolicy())
+                    .isLetterboxedForFixedOrientationAndAspectRatio();
+        }
+
+        void resizeMainWindow(int newWidth, int newHeight) {
+            mWindowState.mRequestedWidth = newWidth;
+            mWindowState.mRequestedHeight = newHeight;
+        }
+
+        void collapseTaskBar() {
+            mTaskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS);
+        }
+
+        void expandTaskBar() {
+            mTaskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
+        }
+
+        void checkWindowStateHasCropBounds(boolean expected) {
+            final Rect cropBounds = getAppCompatLetterboxPolicy().getCropBoundsIfNeeded(
+                    mWindowState);
+            if (expected) {
+                assertNotNull(cropBounds);
+            } else {
+                assertNull(cropBounds);
+            }
+        }
+
+        void checkTaskBarIsExpanded(boolean expected) {
+            final InsetsSource expandedTaskBar = AppCompatUtils.getExpandedTaskbarOrNull(
+                    mWindowState);
+            if (expected) {
+                assertNotNull(expandedTaskBar);
+            } else {
+                assertNull(expandedTaskBar);
+            }
+        }
+
+        void checkWindowStateRoundedCornersRadius(int expected) {
+            assertEquals(expected, getAppCompatLetterboxPolicy()
+                    .getRoundedCornersRadius(mWindowState));
+        }
+
+        void validateWindowStateCropBounds(int left, int top, int right, int bottom) {
+            final Rect cropBounds = getAppCompatLetterboxPolicy().getCropBoundsIfNeeded(
+                    mWindowState);
+            assertEquals(left, cropBounds.left);
+            assertEquals(top, cropBounds.top);
+            assertEquals(right, cropBounds.right);
+            assertEquals(bottom, cropBounds.bottom);
+        }
+
+        @NonNull
+        private AppCompatAspectRatioPolicy getAspectRatioPolicy() {
+            return activity().top().mAppCompatController.getAppCompatAspectRatioPolicy();
+        }
+
+        @NonNull
+        private TransparentPolicy getTransparentPolicy() {
+            return activity().top().mAppCompatController.getTransparentPolicy();
+        }
+
+        @NonNull
+        private AppCompatLetterboxPolicy getAppCompatLetterboxPolicy() {
+            return activity().top().mAppCompatController.getAppCompatLetterboxPolicy();
+        }
+
+    }
+
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatResourcesRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatResourcesRobot.java
new file mode 100644
index 0000000..05e9a0f
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatResourcesRobot.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.mockito.Mockito.doReturn;
+
+import android.content.res.Resources;
+
+import androidx.annotation.DimenRes;
+import androidx.annotation.NonNull;
+
+/**
+ * Robot for managing {@link Resources} in unit tests.
+ */
+public class AppCompatResourcesRobot {
+
+    @NonNull
+    private final Resources mResources;
+
+    AppCompatResourcesRobot(@NonNull Resources resources) {
+        mResources = resources;
+        spyOn(mResources);
+    }
+
+    void configureGetDimensionPixelSize(@DimenRes int resId, int value) {
+        doReturn(value).when(mResources).getDimensionPixelSize(resId);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
index 4e58e1d..5f2a63a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
@@ -37,6 +37,8 @@
     private final AppCompatConfigurationRobot mConfigurationRobot;
     @NonNull
     private final AppCompatComponentPropRobot mOptPropRobot;
+    @NonNull
+    private final AppCompatResourcesRobot mResourcesRobot;
 
     AppCompatRobotBase(@NonNull WindowManagerService wm,
             @NonNull ActivityTaskManagerService atm,
@@ -48,6 +50,7 @@
         mConfigurationRobot =
                 new AppCompatConfigurationRobot(wm.mAppCompatConfiguration);
         mOptPropRobot = new AppCompatComponentPropRobot(wm);
+        mResourcesRobot = new AppCompatResourcesRobot(wm.mContext.getResources());
     }
 
     AppCompatRobotBase(@NonNull WindowManagerService wm,
@@ -60,7 +63,7 @@
      * Specific Robots can override this method to add operation to run on a newly created
      * {@link ActivityRecord}. Common case is to invoke spyOn().
      *
-     * @param activity THe newly created {@link ActivityRecord}.
+     * @param activity The newly created {@link ActivityRecord}.
      */
     @CallSuper
     void onPostActivityCreation(@NonNull ActivityRecord activity) {
@@ -81,7 +84,6 @@
         return mConfigurationRobot;
     }
 
-    @NonNull
     void applyOnConf(@NonNull Consumer<AppCompatConfigurationRobot> consumer) {
         consumer.accept(mConfigurationRobot);
     }
@@ -91,7 +93,6 @@
         return mActivityRobot;
     }
 
-    @NonNull
     void applyOnActivity(@NonNull Consumer<AppCompatActivityRobot> consumer) {
         consumer.accept(mActivityRobot);
     }
@@ -101,8 +102,16 @@
         return mOptPropRobot;
     }
 
-    @NonNull
     void applyOnProp(@NonNull Consumer<AppCompatComponentPropRobot> consumer) {
         consumer.accept(mOptPropRobot);
     }
+
+    @NonNull
+    AppCompatResourcesRobot resources() {
+        return mResourcesRobot;
+    }
+
+    void applyOnResources(@NonNull Consumer<AppCompatResourcesRobot> consumer) {
+        consumer.accept(mResourcesRobot);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
deleted file mode 100644
index 8947522..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ /dev/null
@@ -1,327 +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.server.wm;
-
-import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.Nullable;
-import android.compat.testing.PlatformCompatChangeRule;
-import android.content.ComponentName;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.platform.test.annotations.Presubmit;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.RoundedCorner;
-import android.view.RoundedCorners;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.R;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-/**
- * Test class for {@link LetterboxUiController}.
- *
- * Build/Install/Run:
- * atest WmTests:LetterboxUiControllerTest
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class LetterboxUiControllerTest extends WindowTestsBase {
-    private static final int TASKBAR_COLLAPSED_HEIGHT = 10;
-    private static final int TASKBAR_EXPANDED_HEIGHT = 20;
-    private static final int SCREEN_WIDTH = 200;
-    private static final int SCREEN_HEIGHT = 100;
-    private static final Rect TASKBAR_COLLAPSED_BOUNDS = new Rect(0,
-            SCREEN_HEIGHT - TASKBAR_COLLAPSED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
-    private static final Rect TASKBAR_EXPANDED_BOUNDS = new Rect(0,
-            SCREEN_HEIGHT - TASKBAR_EXPANDED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
-
-    @Rule
-    public TestRule compatChangeRule = new PlatformCompatChangeRule();
-
-    private ActivityRecord mActivity;
-    private Task mTask;
-    private DisplayContent mDisplayContent;
-    private AppCompatConfiguration mAppCompatConfiguration;
-    private final Rect mLetterboxedPortraitTaskBounds = new Rect();
-
-    @Before
-    public void setUp() throws Exception {
-        mActivity = setUpActivityWithComponent();
-
-        mAppCompatConfiguration = mWm.mAppCompatConfiguration;
-        spyOn(mAppCompatConfiguration);
-    }
-
-    @Test
-    public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() {
-        final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
-                WindowInsets.Type.navigationBars());
-        taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
-        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
-        final Rect opaqueBounds = new Rect(0, 0, 500, 300);
-        doReturn(opaqueBounds).when(mActivity).getBounds();
-        // Activity is translucent
-        spyOn(mActivity.mAppCompatController.getTransparentPolicy());
-        when(mActivity.mAppCompatController.getTransparentPolicy()
-                .isRunning()).thenReturn(true);
-
-        // Makes requested sizes different
-        mainWindow.mRequestedWidth = opaqueBounds.width() - 1;
-        mainWindow.mRequestedHeight = opaqueBounds.height() - 1;
-        final AppCompatLetterboxPolicy letterboxPolicy =
-                mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-        assertNull(letterboxPolicy.getCropBoundsIfNeeded(mainWindow));
-
-        // Makes requested sizes equals
-        mainWindow.mRequestedWidth = opaqueBounds.width();
-        mainWindow.mRequestedHeight = opaqueBounds.height();
-        assertNotNull(letterboxPolicy.getCropBoundsIfNeeded(mainWindow));
-    }
-
-    @Test
-    public void testGetCropBoundsIfNeeded_noCrop() {
-        final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
-                WindowInsets.Type.navigationBars());
-        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
-
-        // Do not apply crop if taskbar is collapsed
-        taskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS);
-        assertNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
-
-        mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4,
-                SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT - SCREEN_HEIGHT / 4);
-
-        final AppCompatLetterboxPolicy letterboxPolicy =
-                mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-
-        final Rect noCrop = letterboxPolicy.getCropBoundsIfNeeded(mainWindow);
-        assertNotEquals(null, noCrop);
-        assertEquals(0, noCrop.left);
-        assertEquals(0, noCrop.top);
-        assertEquals(mLetterboxedPortraitTaskBounds.width(), noCrop.right);
-        assertEquals(mLetterboxedPortraitTaskBounds.height(), noCrop.bottom);
-    }
-
-    @Test
-    public void testGetCropBoundsIfNeeded_appliesCrop() {
-        final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
-                WindowInsets.Type.navigationBars());
-        taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
-        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
-
-        // Apply crop if taskbar is expanded
-        taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
-        assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
-
-        mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
-                SCREEN_HEIGHT);
-
-        final AppCompatLetterboxPolicy letterboxPolicy =
-                mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-        final Rect crop = letterboxPolicy.getCropBoundsIfNeeded(mainWindow);
-        assertNotEquals(null, crop);
-        assertEquals(0, crop.left);
-        assertEquals(0, crop.top);
-        assertEquals(mLetterboxedPortraitTaskBounds.width(), crop.right);
-        assertEquals(mLetterboxedPortraitTaskBounds.height() - TASKBAR_EXPANDED_HEIGHT,
-                crop.bottom);
-    }
-
-    @Test
-    public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() {
-        final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
-                WindowInsets.Type.navigationBars());
-        taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
-        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
-        final float scaling = 2.0f;
-
-        // Apply crop if taskbar is expanded
-        taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
-        assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
-        // With SizeCompat scaling
-        doReturn(true).when(mActivity).inSizeCompatMode();
-        mainWindow.mInvGlobalScale = scaling;
-
-        mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
-                SCREEN_HEIGHT);
-
-        final int appWidth = mLetterboxedPortraitTaskBounds.width();
-        final int appHeight = mLetterboxedPortraitTaskBounds.height();
-
-        final AppCompatLetterboxPolicy letterboxPolicy =
-                mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-        final Rect crop = letterboxPolicy.getCropBoundsIfNeeded(mainWindow);
-        assertNotEquals(null, crop);
-        assertEquals(0, crop.left);
-        assertEquals(0, crop.top);
-        assertEquals((int) (appWidth * scaling), crop.right);
-        assertEquals((int) ((appHeight - TASKBAR_EXPANDED_HEIGHT) * scaling), crop.bottom);
-    }
-
-    @Test
-    public void testGetRoundedCornersRadius_withRoundedCornersFromInsets() {
-        final float invGlobalScale = 0.5f;
-        final int expectedRadius = 7;
-        final int configurationRadius = 15;
-
-        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
-        mainWindow.mInvGlobalScale = invGlobalScale;
-        final InsetsState insets = mainWindow.getInsetsState();
-
-        RoundedCorners roundedCorners = new RoundedCorners(
-                /*topLeft=*/ null,
-                /*topRight=*/ null,
-                /*bottomRight=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT,
-                    configurationRadius, /*centerX=*/ 1, /*centerY=*/ 1),
-                /*bottomLeft=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT,
-                    configurationRadius * 2 /*2 is to test selection of the min radius*/,
-                    /*centerX=*/ 1, /*centerY=*/ 1)
-        );
-        insets.setRoundedCorners(roundedCorners);
-        mAppCompatConfiguration.setLetterboxActivityCornersRadius(-1);
-
-        assertEquals(expectedRadius, mActivity.mAppCompatController.getAppCompatLetterboxPolicy()
-                .getRoundedCornersRadius(mainWindow));
-    }
-
-    @Test
-    public void testGetRoundedCornersRadius_withLetterboxActivityCornersRadius() {
-        final float invGlobalScale = 0.5f;
-        final int expectedRadius = 7;
-        final int configurationRadius = 15;
-
-        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
-        mainWindow.mInvGlobalScale = invGlobalScale;
-        mAppCompatConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
-
-        final AppCompatLetterboxPolicy letterboxPolicy =
-                mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-
-        doReturn(true).when(mActivity).isInLetterboxAnimation();
-        assertEquals(expectedRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
-        doReturn(false).when(mActivity).isInLetterboxAnimation();
-        assertEquals(expectedRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
-        doReturn(false).when(mActivity).isVisibleRequested();
-        doReturn(false).when(mActivity).isVisible();
-        assertEquals(0, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
-        doReturn(true).when(mActivity).isInLetterboxAnimation();
-        assertEquals(expectedRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-    }
-
-    @Test
-    public void testGetRoundedCornersRadius_noScalingApplied() {
-        final int configurationRadius = 15;
-
-        final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
-        mAppCompatConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
-
-        final AppCompatLetterboxPolicy letterboxPolicy =
-                mActivity.mAppCompatController.getAppCompatLetterboxPolicy();
-
-        mainWindow.mInvGlobalScale = -1f;
-        assertEquals(configurationRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
-        mainWindow.mInvGlobalScale = 0f;
-        assertEquals(configurationRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-
-        mainWindow.mInvGlobalScale = 1f;
-        assertEquals(configurationRadius, letterboxPolicy.getRoundedCornersRadius(mainWindow));
-    }
-
-    private WindowState mockForGetCropBoundsAndRoundedCorners(@Nullable InsetsSource taskbar) {
-        final WindowState mainWindow = mock(WindowState.class);
-        final InsetsState insets = new InsetsState();
-        final Resources resources = mWm.mContext.getResources();
-        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
-
-        final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivity.mAppCompatController
-                .getAppCompatAspectRatioPolicy();
-
-        mainWindow.mInvGlobalScale = 1f;
-        spyOn(resources);
-        spyOn(mActivity);
-        spyOn(aspectRatioPolicy);
-
-        if (taskbar != null) {
-            taskbar.setVisible(true);
-            insets.addSource(taskbar);
-        }
-        doReturn(mLetterboxedPortraitTaskBounds).when(mActivity).getBounds();
-        doReturn(false).when(mActivity).isInLetterboxAnimation();
-        doReturn(true).when(mActivity).isVisible();
-        doReturn(true).when(aspectRatioPolicy)
-                .isLetterboxedForFixedOrientationAndAspectRatio();
-        doReturn(insets).when(mainWindow).getInsetsState();
-        doReturn(attrs).when(mainWindow).getAttrs();
-        doReturn(true).when(mainWindow).isDrawn();
-        doReturn(true).when(mainWindow).isOnScreen();
-        doReturn(false).when(mainWindow).isLetterboxedForDisplayCutout();
-        doReturn(true).when(mainWindow).areAppWindowBoundsLetterboxed();
-        doReturn(true).when(mAppCompatConfiguration).isLetterboxActivityCornersRounded();
-        doReturn(TASKBAR_EXPANDED_HEIGHT).when(resources).getDimensionPixelSize(
-                R.dimen.taskbar_frame_height);
-
-        return mainWindow;
-    }
-
-    @Test
-    public void testIsLetterboxEducationEnabled() {
-        mActivity.mAppCompatController.getAppCompatLetterboxOverrides()
-                .isLetterboxEducationEnabled();
-        verify(mAppCompatConfiguration).getIsEducationEnabled();
-    }
-
-    private ActivityRecord setUpActivityWithComponent() {
-        mDisplayContent = new TestDisplayContent
-                .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
-        mTask = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build();
-        final ActivityRecord activity = new ActivityBuilder(mAtm)
-                .setOnTop(true)
-                .setTask(mTask)
-                // Set the component to be that of the test class in order to enable compat changes
-                .setComponent(ComponentName.createRelative(mContext,
-                        com.android.server.wm.LetterboxUiControllerTest.class.getName()))
-                .build();
-        spyOn(activity.mAppCompatController.getAppCompatCameraOverrides());
-        return activity;
-    }
-}
diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
index 4143f59..30cc002 100644
--- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
+++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
@@ -171,11 +171,11 @@
                 .setConsumedPower(123)
                 .setConsumedPower(
                         BatteryConsumer.POWER_COMPONENT_CPU, 10100)
-                .setConsumedPowerForCustomComponent(
+                .setConsumedPower(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10200)
                 .setUsageDurationMillis(
                         BatteryConsumer.POWER_COMPONENT_CPU, 10300)
-                .setUsageDurationForCustomComponentMillis(
+                .setUsageDurationMillis(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10400);
 
         for (int i = 0; i < 1000; i++) {
@@ -191,10 +191,9 @@
                 consumerBuilder.setUsageDurationMillis(componentId, componentId * 1000);
             }
 
-            consumerBuilder.setConsumedPowerForCustomComponent(
-                    BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 1234)
-                    .setUsageDurationForCustomComponentMillis(
-                            BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 4321);
+            consumerBuilder
+                    .setConsumedPower(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 1234)
+                    .setUsageDurationMillis(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 4321);
         }
         return builder.build();
     }