Merge "Add aconfig flag for audio input routing/volume control" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 59a7cbc..b4127c5 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -691,7 +691,7 @@
     exportable: true,
     package: "android.media.tv.flags",
     container: "system",
-    srcs: ["media/java/android/media/tv/flags/media_tv.aconfig"],
+    srcs: ["media/java/android/media/tv/flags/*.aconfig"],
 }
 
 java_aconfig_library {
diff --git a/Android.bp b/Android.bp
index af205d8..f8907f3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -109,7 +109,7 @@
         ":android.hardware.security.keymint-V3-java-source",
         ":android.hardware.security.secureclock-V1-java-source",
         ":android.hardware.thermal-V2-java-source",
-        ":android.hardware.tv.tuner-V2-java-source",
+        ":android.hardware.tv.tuner-V3-java-source",
         ":android.security.apc-java-source",
         ":android.security.authorization-java-source",
         ":android.security.legacykeystore-java-source",
diff --git a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
index e82df12..36b6c80 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
+++ b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
@@ -16,12 +16,7 @@
             ]
         },
         {
-            "name": "FrameworksServicesTests",
-            "options": [
-                {"include-filter": "com.android.server.job"},
-                {"exclude-annotation": "androidx.test.filters.FlakyTest"},
-                {"exclude-annotation": "androidx.test.filters.LargeTest"}
-            ]
+            "name": "FrameworksServicesTests_com_android_server_job_Presubmit"
         }
     ],
     "postsubmit": [
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
index a75415e..52670a2 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
+++ b/apex/jobscheduler/service/java/com/android/server/usage/TEST_MAPPING
@@ -17,11 +17,7 @@
       ]
     },
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {"include-filter": "com.android.server.usage"},
-        {"exclude-annotation": "androidx.test.filters.FlakyTest"}
-      ]
+      "name": "FrameworksServicesTests_com_android_server_usage_Presubmit"
     }
   ],
   "postsubmit": [
diff --git a/core/api/current.txt b/core/api/current.txt
index 66a1d30..c06b814 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8724,6 +8724,13 @@
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager {
   }
 
+  @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
+    ctor public AppFunctionService();
+    method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
+  }
+
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class ExecuteAppFunctionRequest implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public android.os.Bundle getExtras();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index fe792bc..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;
   }
@@ -8140,6 +8142,7 @@
     field public static final int TUNER_VERSION_1_1 = 65537; // 0x10001
     field public static final int TUNER_VERSION_2_0 = 131072; // 0x20000
     field public static final int TUNER_VERSION_3_0 = 196608; // 0x30000
+    field @FlaggedApi("android.media.tv.flags.tuner_w_apis") public static final int TUNER_VERSION_4_0 = 262144; // 0x40000
     field public static final int TUNER_VERSION_UNKNOWN = 0; // 0x0
   }
 
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index c876921..0a05144 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -68,7 +68,6 @@
 import android.os.StrictMode;
 import android.os.WorkSource;
 import android.service.voice.IVoiceInteractionSession;
-import android.view.IRecentsAnimationRunner;
 import android.view.IRemoteAnimationRunner;
 import android.view.RemoteAnimationDefinition;
 import android.view.RemoteAnimationAdapter;
@@ -129,13 +128,12 @@
     int startActivityFromGameSession(IApplicationThread caller, in String callingPackage,
             in String callingFeatureId, int callingPid, int callingUid, in Intent intent,
             int taskId, int userId);
-    void startRecentsActivity(in Intent intent, in long eventTime,
-            in IRecentsAnimationRunner recentsAnimationRunner);
     int startActivityFromRecents(int taskId, in Bundle options);
     int startActivityAsCaller(in IApplicationThread caller, in String callingPackage,
             in Intent intent, in String resolvedType, in IBinder resultTo, in String resultWho,
             int requestCode, int flags, in ProfilerInfo profilerInfo, in Bundle options,
             boolean ignoreTargetSecurity, int userId);
+    void preloadRecentsActivity(in Intent intent);
 
     boolean isActivityStartAllowedOnDisplay(int displayId, in Intent intent, in String resolvedType,
             int userId);
@@ -167,7 +165,6 @@
     /** Focuses the top task on a display if it isn't already focused. Used for Recents. */
     void focusTopTask(int displayId);
 
-    void cancelRecentsAnimation(boolean restoreHomeRootTaskPosition);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.UPDATE_LOCK_TASK_PACKAGES)")
     void updateLockTaskPackages(int userId, in String[] packages);
     boolean isInLockTaskMode();
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 69b5222..83f9ff7 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -393,7 +393,7 @@
      * changes.
      *
      * <p>This broadcast is only sent to registered receivers and receivers in packages that have
-     * been granted Do Not Disturb access (see {@link #isNotificationPolicyAccessGranted()}).
+     * been granted Notification Policy access (see {@link #isNotificationPolicyAccessGranted()}).
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
@@ -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
      */
@@ -1627,7 +1653,7 @@
     }
 
     /**
-     * Checks the ability to modify notification do not disturb policy for the calling package.
+     * Checks the ability to modify Notification Policy for the calling package.
      *
      * <p>
      * Returns true if the calling package can modify notification policy.
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index b7f672c..2358d67 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -29,12 +29,7 @@
         },
         {
             "file_patterns": ["(/|^)AppOpsManager.java"],
-            "name": "FrameworksServicesTests",
-            "options": [
-                {
-                    "include-filter": "com.android.server.appop"
-                }
-            ]
+            "name": "FrameworksServicesTests_android_server_appop"
         },
         {
             "file_patterns": ["(/|^)AppOpsManager.java"],
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/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
new file mode 100644
index 0000000..fca465f
--- /dev/null
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appfunctions;
+
+import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE;
+
+import android.annotation.FlaggedApi;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import java.util.function.Consumer;
+
+/**
+ * Abstract base class to provide app functions to the system.
+ *
+ * <p>Include the following in the manifest:
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourService"
+ *       android:permission="android.permission.BIND_APP_FUNCTION_SERVICE">
+ *    <intent-filter>
+ *      <action android:name="android.app.appfunctions.AppFunctionService" />
+ *    </intent-filter>
+ * </service>
+ * }
+ * </pre>
+ *
+ * @see AppFunctionManager
+ */
+@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
+public abstract class AppFunctionService extends Service {
+    /**
+     * The {@link Intent} that must be declared as handled by the service. To be supported, the
+     * service must also require the {@link BIND_APP_FUNCTION_SERVICE} permission so that other
+     * applications can not abuse it.
+     */
+    @NonNull
+    public static final String SERVICE_INTERFACE =
+            "android.app.appfunctions.AppFunctionService";
+
+    private final Binder mBinder =
+            new IAppFunctionService.Stub() {
+                @Override
+                public void executeAppFunction(
+                        @NonNull ExecuteAppFunctionRequest request,
+                        @NonNull IExecuteAppFunctionCallback callback) {
+                    if (AppFunctionService.this.checkCallingPermission(
+                            BIND_APP_FUNCTION_SERVICE) == PERMISSION_DENIED) {
+                        throw new SecurityException("Can only be called by the system server.");
+                    }
+                    SafeOneTimeExecuteAppFunctionCallback safeCallback =
+                            new SafeOneTimeExecuteAppFunctionCallback(callback);
+                    try {
+                        AppFunctionService.this.onExecuteFunction(
+                                request,
+                                safeCallback::onResult);
+                    } catch (Exception ex) {
+                        // Apps should handle exceptions. But if they don't, report the error on
+                        // behalf of them.
+                        safeCallback.onResult(
+                                new ExecuteAppFunctionResponse.Builder(
+                                        getResultCode(ex), ex.getMessage()).build());
+                    }
+                }
+            };
+
+    private static int getResultCode(@NonNull Throwable t) {
+        if (t instanceof IllegalArgumentException) {
+            return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
+        }
+        return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
+    }
+
+    @NonNull
+    @Override
+    public final IBinder onBind(@Nullable Intent intent) {
+        return mBinder;
+    }
+
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * <p>This method is triggered when the system requests your AppFunctionService to handle a
+     * particular function you have registered and made available.
+     *
+     * <p>To ensure proper routing of function requests, assign a unique identifier to each
+     * function. This identifier doesn't need to be globally unique, but it must be unique within
+     * your app. For example, a function to order food could be identified as "orderFood". In most
+     * cases this identifier should come from the ID automatically generated by the AppFunctions
+     * SDK. You can determine the specific function to invoke by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     *
+     * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+     * thread and dispatch the result with the given callback. You should always report back the
+     * result using the callback, no matter if the execution was successful or not.
+     *
+     * @param request The function execution request.
+     * @param callback A callback to report back the result.
+     */
+    @MainThread
+    public abstract void onExecuteFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+}
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/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl
new file mode 100644
index 0000000..12b5c55
--- /dev/null
+++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appfunctions;
+
+import android.os.Bundle;
+import android.app.appfunctions.IExecuteAppFunctionCallback;
+import android.app.appfunctions.ExecuteAppFunctionRequest;
+
+
+ /** {@hide} */
+oneway interface IAppFunctionService {
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * @param request  the function execution request.
+     * @param callback a callback to report back the result.
+     */
+    void executeAppFunction(
+        in ExecuteAppFunctionRequest request,
+        in IExecuteAppFunctionCallback callback
+    );
+}
diff --git a/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl b/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl
new file mode 100644
index 0000000..5323f9b
--- /dev/null
+++ b/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appfunctions;
+
+import android.app.appfunctions.ExecuteAppFunctionResponse;
+
+/** {@hide} */
+oneway interface IExecuteAppFunctionCallback {
+    void onResult(in ExecuteAppFunctionResponse result);
+}
diff --git a/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java b/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java
new file mode 100644
index 0000000..86fc369
--- /dev/null
+++ b/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appfunctions;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+/**
+ * A wrapper of IExecuteAppFunctionCallback which swallows the {@link RemoteException}. This
+ * callback is intended for one-time use only. Subsequent calls to onResult() will be ignored.
+ *
+ * @hide
+ */
+public class SafeOneTimeExecuteAppFunctionCallback {
+    private static final String TAG = "SafeOneTimeExecuteApp";
+
+    private final AtomicBoolean mOnResultCalled = new AtomicBoolean(false);
+
+    @NonNull private final IExecuteAppFunctionCallback mCallback;
+
+    @Nullable private final Consumer<ExecuteAppFunctionResponse> mOnDispatchCallback;
+
+    public SafeOneTimeExecuteAppFunctionCallback(@NonNull IExecuteAppFunctionCallback callback) {
+        this(callback, /* onDispatchCallback= */ null);
+    }
+
+    /**
+     * @param callback The callback to wrap.
+     * @param onDispatchCallback An optional callback invoked after the wrapped callback has been
+     *     dispatched with a result. This callback receives the result that has been dispatched.
+     */
+    public SafeOneTimeExecuteAppFunctionCallback(
+            @NonNull IExecuteAppFunctionCallback callback,
+            @Nullable Consumer<ExecuteAppFunctionResponse> onDispatchCallback) {
+        mCallback = Objects.requireNonNull(callback);
+        mOnDispatchCallback = onDispatchCallback;
+    }
+
+    /** Invoke wrapped callback with the result. */
+    public void onResult(@NonNull ExecuteAppFunctionResponse result) {
+        if (!mOnResultCalled.compareAndSet(false, true)) {
+            Log.w(TAG, "Ignore subsequent calls to onResult()");
+            return;
+        }
+        try {
+            mCallback.onResult(result);
+        } catch (RemoteException ex) {
+            // Failed to notify the other end. Ignore.
+            Log.w(TAG, "Failed to invoke the callback", ex);
+        }
+        if (mOnDispatchCallback != null) {
+            mOnDispatchCallback.accept(result);
+        }
+    }
+}
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/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index c7716e5..ffadd1e 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -112,6 +112,39 @@
                     "exclude-annotation":"org.junit.Ignore"
                 }
             ]
+        },
+        {
+            "name": "CtsPackageInstallerCUJInstallationTestCases",
+            "options":[
+               {
+                   "exclude-annotation":"androidx.test.filters.FlakyTest"
+               },
+               {
+                   "exclude-annotation":"org.junit.Ignore"
+               }
+            ]
+        },
+        {
+            "name": "CtsPackageInstallerCUJUninstallationTestCases",
+            "options":[
+               {
+                   "exclude-annotation":"androidx.test.filters.FlakyTest"
+               },
+               {
+                   "exclude-annotation":"org.junit.Ignore"
+               }
+            ]
+        },
+        {
+            "name": "CtsPackageInstallerCUJUpdateSelfTestCases",
+            "options":[
+               {
+                   "exclude-annotation":"androidx.test.filters.FlakyTest"
+               },
+               {
+                   "exclude-annotation":"org.junit.Ignore"
+               }
+            ]
         }
     ],
     "presubmit-large":[
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/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index 449a52f..728db27 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -82,10 +82,7 @@
         "PowerComponents\\.java",
         "[^/]*BatteryConsumer[^/]*\\.java"
       ],
-      "name": "FrameworksServicesTests",
-      "options": [
-        { "include-filter": "com.android.server.am.BatteryStatsServiceTest" }
-      ]
+      "name": "FrameworksServicesTests_battery_stats"
     },
     {
       "file_patterns": [
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 184bac4..85d2325 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1972,10 +1972,10 @@
             "android.provider.extra.NOTIFICATION_LISTENER_COMPONENT_NAME";
 
     /**
-     * Activity Action: Show Do Not Disturb access settings.
+     * Activity Action: Show Notification Policy access settings.
      * <p>
-     * Users can grant and deny access to Do Not Disturb configuration from here. Managed
-     * profiles cannot grant Do Not Disturb access.
+     * Users can grant and deny access to Notification Policy (DND / Priority Modes) configuration
+     * from here. Managed profiles cannot grant Notification Policy access.
      * See {@link android.app.NotificationManager#isNotificationPolicyAccessGranted()} for more
      * details.
      * <p>
diff --git a/core/java/android/security/advancedprotection/OWNERS b/core/java/android/security/advancedprotection/OWNERS
new file mode 100644
index 0000000..ddac8ed
--- /dev/null
+++ b/core/java/android/security/advancedprotection/OWNERS
@@ -0,0 +1,12 @@
+# Bug component: 315013
+
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
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/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 1c3d738..40070c7 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -259,6 +259,16 @@
 }
 
 flag {
+  name: "dont_break_email_in_nobreak_tag"
+  namespace: "text"
+  description: "Prevent line break inside email."
+  bug: "350691716"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "handwriting_gesture_with_transformation"
   namespace: "text"
   description: "Fix handwriting gesture is not working when view has transformation."
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/java/com/android/internal/os/TEST_MAPPING b/core/java/com/android/internal/os/TEST_MAPPING
index 8346d71..154da5c 100644
--- a/core/java/com/android/internal/os/TEST_MAPPING
+++ b/core/java/com/android/internal/os/TEST_MAPPING
@@ -28,10 +28,7 @@
         "Kernel[^/]*\\.java",
         "[^/]*Power[^/]*\\.java"
       ],
-      "name": "FrameworksServicesTests",
-      "options": [
-        { "include-filter": "com.android.server.am.BatteryStatsServiceTest" }
-      ]
+      "name": "FrameworksServicesTests_battery_stats"
     },
     {
       "file_patterns": [
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 160f651..f795406 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -830,7 +830,6 @@
     <protected-broadcast android:name="android.intent.action.PROFILE_REMOVED" />
     <protected-broadcast android:name="com.android.internal.telephony.cat.SMS_SENT_ACTION" />
     <protected-broadcast android:name="com.android.internal.telephony.cat.SMS_DELIVERY_ACTION" />
-    <protected-broadcast android:name="com.android.internal.telephony.data.ACTION_RETRY" />
     <protected-broadcast android:name="android.companion.virtual.action.VIRTUAL_DEVICE_REMOVED" />
     <protected-broadcast android:name="com.android.internal.intent.action.FLASH_NOTIFICATION_START_PREVIEW" />
     <protected-broadcast android:name="com.android.internal.intent.action.FLASH_NOTIFICATION_STOP_PREVIEW" />
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/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 383033d..118acac 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -282,6 +282,11 @@
     <string name="config_satellite_emergency_handover_intent_action" translatable="false"></string>
     <java-symbol type="string" name="config_satellite_emergency_handover_intent_action" />
 
+    <!-- The action of the intent that hidden menu sends to the app to launch demo mode for sos
+     emergency messaging via satellite. -->
+    <string name="config_satellite_demo_mode_sos_intent_action" translatable="false"></string>
+    <java-symbol type="string" name="config_satellite_demo_mode_sos_intent_action" />
+
     <!-- Whether outgoing satellite datagrams should be sent to modem in demo mode. When satellite
          is enabled for demo mode, if this config is enabled, outgoing datagrams will be sent to
          modem; otherwise, success results will be returned. If demo mode is disabled, outgoing
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index a115c65..38ea4ac 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -93,5 +93,6 @@
         <permission name="android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS" />
         <permission name="android.permission.CONTROL_UI_TRACING" />
         <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" />
+        <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW"/>
     </privapp-permissions>
 </permissions>
diff --git a/data/fonts/font_fallback.xml b/data/fonts/font_fallback.xml
index 53024ab..ae50da1 100644
--- a/data/fonts/font_fallback.xml
+++ b/data/fonts/font_fallback.xml
@@ -304,7 +304,7 @@
         <font postScriptName="NotoSansBengali-Regular" supportedAxes="wght">
             NotoSansBengali-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular" supportedAxes="wght">
             NotoSerifBengali-VF.ttf
         </font>
     </family>
@@ -354,7 +354,7 @@
         <font postScriptName="NotoSansSinhala-Regular" supportedAxes="wght">
             NotoSansSinhala-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular" supportedAxes="wght">
             NotoSerifSinhala-VF.ttf
         </font>
     </family>
@@ -927,23 +927,23 @@
             NotoSansMedefaidrin-VF.ttf
         </font>
     </family>
-    <family lang="und-Soyo" supportedAxes="wght">
-        <font postScriptName="NotoSansSoyombo-Regular">
+    <family lang="und-Soyo">
+        <font postScriptName="NotoSansSoyombo-Regular" supportedAxes="wght">
             NotoSansSoyombo-VF.ttf
         </font>
     </family>
-    <family lang="und-Takr" supportedAxes="wght">
-        <font postScriptName="NotoSansTakri-Regular">
+    <family lang="und-Takr">
+        <font postScriptName="NotoSansTakri-Regular" supportedAxes="wght">
             NotoSansTakri-VF.ttf
         </font>
     </family>
-    <family lang="und-Hmnp" supportedAxes="wght">
-        <font postScriptName="NotoSerifHmongNyiakeng-Regular">
+    <family lang="und-Hmnp">
+        <font postScriptName="NotoSerifHmongNyiakeng-Regular" supportedAxes="wght">
             NotoSerifNyiakengPuachueHmong-VF.ttf
         </font>
     </family>
-    <family lang="und-Yezi" supportedAxes="wght">
-        <font postScriptName="NotoSerifYezidi-Regular">
+    <family lang="und-Yezi">
+        <font postScriptName="NotoSerifYezidi-Regular" supportedAxes="wght">
             NotoSerifYezidi-VF.ttf
         </font>
     </family>
diff --git a/data/fonts/font_fallback_cjkvf.xml b/data/fonts/font_fallback_cjkvf.xml
index a4ee825..407d704 100644
--- a/data/fonts/font_fallback_cjkvf.xml
+++ b/data/fonts/font_fallback_cjkvf.xml
@@ -304,7 +304,7 @@
         <font postScriptName="NotoSansBengali-Regular" supportedAxes="wght">
             NotoSansBengali-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular" supportedAxes="wght">
             NotoSerifBengali-VF.ttf
         </font>
     </family>
@@ -354,7 +354,7 @@
         <font postScriptName="NotoSansSinhala-Regular" supportedAxes="wght">
             NotoSansSinhala-VF.ttf
         </font>
-        <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular">
+        <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular" supportedAxes="wght">
             NotoSerifSinhala-VF.ttf
         </font>
     </family>
@@ -943,23 +943,23 @@
             NotoSansMedefaidrin-VF.ttf
         </font>
     </family>
-    <family lang="und-Soyo" supportedAxes="wght">
-        <font postScriptName="NotoSansSoyombo-Regular">
+    <family lang="und-Soyo">
+        <font postScriptName="NotoSansSoyombo-Regular" supportedAxes="wght">
             NotoSansSoyombo-VF.ttf
         </font>
     </family>
-    <family lang="und-Takr" supportedAxes="wght">
-        <font postScriptName="NotoSansTakri-Regular">
+    <family lang="und-Takr">
+        <font postScriptName="NotoSansTakri-Regular" supportedAxes="wght">
             NotoSansTakri-VF.ttf
         </font>
     </family>
-    <family lang="und-Hmnp" supportedAxes="wght">
-        <font postScriptName="NotoSerifHmongNyiakeng-Regular">
+    <family lang="und-Hmnp">
+        <font postScriptName="NotoSerifHmongNyiakeng-Regular" supportedAxes="wght">
             NotoSerifNyiakengPuachueHmong-VF.ttf
         </font>
     </family>
-    <family lang="und-Yezi" supportedAxes="wght">
-        <font postScriptName="NotoSerifYezidi-Regular">
+    <family lang="und-Yezi">
+        <font postScriptName="NotoSerifYezidi-Regular" supportedAxes="wght">
             NotoSerifYezidi-VF.ttf
         </font>
     </family>
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
index 027b28e..991cdcf 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
index 027b28e..991cdcf 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
Binary files differ
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/res/drawable/decor_back_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml
index 5ecba38..a36b21f 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml
@@ -15,6 +15,7 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:autoMirrored="true"
         android:width="32.0dp"
         android:height="32.0dp"
         android:viewportWidth="32.0"
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/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index b3beb4a..2de545a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -695,6 +695,16 @@
                 return;
             }
 
+            if (mSplitScreenOptional.isPresent()) {
+                // If pip activity will reparent to origin task case and if the origin task still
+                // under split root, apply exit split transaction to make it expand to fullscreen.
+                SplitScreenController split = mSplitScreenOptional.get();
+                if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) {
+                    split.prepareExitSplitScreen(wct, split.getStageOfTask(
+                            mTaskInfo.lastParentTaskIdBeforePip),
+                            SplitScreenController.EXIT_REASON_APP_FINISHED);
+                }
+            }
             mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds);
             return;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index d8dba71..501e856 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.content.pm.PackageManager.FEATURE_PC;
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
 import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -174,6 +175,11 @@
 
         if (decoration == null) return;
 
+        if (!shouldShowWindowDecor(taskInfo)) {
+            destroyWindowDecoration(taskInfo);
+            return;
+        }
+
         decoration.relayout(taskInfo);
         setupCaptionColor(taskInfo, decoration);
     }
@@ -250,6 +256,9 @@
         if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
             return true;
         }
+        if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
+            return false;
+        }
         if (taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) {
             return false;
         }
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/AudioManager.java b/media/java/android/media/AudioManager.java
index c36eda90..ca468fc 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1027,7 +1027,7 @@
      * <p>This method has no effect if the device implements a fixed volume policy
      * as indicated by {@link #isVolumeFixed()}.
      * <p>From N onward, ringer mode adjustments that would toggle Do Not Disturb are not allowed
-     * unless the app has been granted Do Not Disturb Access.
+     * unless the app has been granted Notification Policy Access.
      * See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
      *
      * @param streamType The stream type to adjust. One of {@link #STREAM_VOICE_CALL},
@@ -1379,7 +1379,7 @@
      * <p>This method has no effect if the device implements a fixed volume policy
      * as indicated by {@link #isVolumeFixed()}.
      * * <p>From N onward, ringer mode adjustments that would toggle Do Not Disturb are not allowed
-     * unless the app has been granted Do Not Disturb Access.
+     * unless the app has been granted Notification Policy Access.
      * See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
      * @param ringerMode The ringer mode, one of {@link #RINGER_MODE_NORMAL},
      *            {@link #RINGER_MODE_SILENT}, or {@link #RINGER_MODE_VIBRATE}.
@@ -1403,7 +1403,7 @@
      * <p>This method has no effect if the device implements a fixed volume policy
      * as indicated by {@link #isVolumeFixed()}.
      * <p>From N onward, volume adjustments that would toggle Do Not Disturb are not allowed unless
-     * the app has been granted Do Not Disturb Access.
+     * the app has been granted Notification Policy Access.
      * See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
      * @param streamType The stream whose volume index should be set.
      * @param index The volume index to set. See
@@ -8829,7 +8829,7 @@
      * <p>This method has no effect if the device implements a fixed volume policy
      * as indicated by {@link #isVolumeFixed()}.
      * <p>From N onward, ringer mode adjustments that would toggle Do Not Disturb are not allowed
-     * unless the app has been granted Do Not Disturb Access.
+     * unless the app has been granted Notification Policy Access.
      * See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
      * <p>This API checks if the caller has the necessary permissions based on the provided
      * component name, uid, and pid values.
@@ -8870,7 +8870,7 @@
      * <p>This method has no effect if the device implements a fixed volume policy
      * as indicated by {@link #isVolumeFixed()}.
      * <p>From N onward, volume adjustments that would toggle Do Not Disturb are not allowed unless
-     * the app has been granted Do Not Disturb Access.
+     * the app has been granted Notification Policy Access.
      * See {@link NotificationManager#isNotificationPolicyAccessGranted()}.
      * <p>This API checks if the caller has the necessary permissions based on the provided
      * component name, uid, and pid values.
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/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java
index e604cb7..82e6ed3 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -91,7 +91,7 @@
     private static final Object sMainTvViewLock = new Object();
     private static WeakReference<TvView> sMainTvView = NULL_TV_VIEW;
 
-    private final Handler mHandler = new Handler();
+    private Handler mHandler = new Handler();
     private Session mSession;
     private SurfaceView mSurfaceView;
     private Surface mSurface;
@@ -207,6 +207,17 @@
         mCallback = callback;
     }
 
+    /**
+     * Sets the handler to be invoked when an event is dispatched to this TvView.
+     * If handler is not set by this function, TvView will use its default handler.
+     *
+     * @param handler The handler to handle events.
+     * @hide
+     */
+    public void setHandler(@NonNull Handler handler) {
+        mHandler = handler;
+    }
+
     /** @hide */
     public Session getInputSession() {
         return mSession;
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index d6e9e4e..0829a90e 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -39,4 +39,12 @@
     namespace: "media_tv"
     description: "Performance and Storage Optimization in Google TV Kids Mode."
     bug: "288383796"
-}
\ No newline at end of file
+}
+
+flag {
+    name: "tuner_w_apis"
+    is_exported: true
+    namespace: "media_tv"
+    description: "Tuner V4.0 APIs for Android W"
+    bug: "320419647"
+}
diff --git a/media/java/android/media/tv/tuner/TunerVersionChecker.java b/media/java/android/media/tv/tuner/TunerVersionChecker.java
index f29a93c..a7c0415 100644
--- a/media/java/android/media/tv/tuner/TunerVersionChecker.java
+++ b/media/java/android/media/tv/tuner/TunerVersionChecker.java
@@ -16,9 +16,11 @@
 
 package android.media.tv.tuner;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.media.tv.flags.Flags;
 import android.util.Log;
 
 import java.lang.annotation.Retention;
@@ -40,7 +42,7 @@
     /** @hide */
     @IntDef(prefix = "TUNER_VERSION_",
             value = {TUNER_VERSION_UNKNOWN, TUNER_VERSION_1_0, TUNER_VERSION_1_1,
-                    TUNER_VERSION_2_0})
+                    TUNER_VERSION_2_0, TUNER_VERSION_3_0, TUNER_VERSION_4_0})
     @Retention(RetentionPolicy.SOURCE)
     public @interface TunerVersion {}
     /**
@@ -63,6 +65,11 @@
      * Tuner version 3.0.
      */
     public static final int TUNER_VERSION_3_0 = (3 << 16);
+    /**
+     * Tuner version 4.0.
+     */
+    @FlaggedApi(Flags.FLAG_TUNER_W_APIS)
+    public static final int TUNER_VERSION_4_0 = (4 << 16);
 
     /**
      * Get the current running Tuner version.
diff --git a/media/jni/Android.bp b/media/jni/Android.bp
index 7f487e5..c44e26f 100644
--- a/media/jni/Android.bp
+++ b/media/jni/Android.bp
@@ -92,7 +92,7 @@
         "[email protected]",
         "[email protected]",
         "android.hardware.drm-V1-ndk",
-        "android.hardware.tv.tuner-V2-ndk",
+        "android.hardware.tv.tuner-V3-ndk",
     ],
 
     header_libs: [
@@ -180,7 +180,7 @@
 
     shared_libs: [
         "[email protected]",
-        "android.hardware.tv.tuner-V2-ndk",
+        "android.hardware.tv.tuner-V3-ndk",
         "libbinder_ndk",
         "libandroid_runtime",
         "libcutils",
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/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING
index da6efee..717ec02 100644
--- a/packages/PackageInstaller/TEST_MAPPING
+++ b/packages/PackageInstaller/TEST_MAPPING
@@ -1,4 +1,39 @@
 {
+  "presubmit": [
+    {
+      "name": "CtsPackageInstallerCUJInstallationTestCases",
+      "options":[
+        {
+          "exclude-annotation":"androidx.test.filters.FlakyTest"
+        },
+        {
+          "exclude-annotation":"org.junit.Ignore"
+        }
+      ]
+    },
+    {
+      "name": "CtsPackageInstallerCUJUninstallationTestCases",
+      "options":[
+        {
+          "exclude-annotation":"androidx.test.filters.FlakyTest"
+        },
+        {
+          "exclude-annotation":"org.junit.Ignore"
+        }
+      ]
+    },
+    {
+      "name": "CtsPackageInstallerCUJUpdateSelfTestCases",
+      "options":[
+        {
+          "exclude-annotation":"androidx.test.filters.FlakyTest"
+        },
+        {
+          "exclude-annotation":"org.junit.Ignore"
+        }
+      ]
+    }
+  ],
   "postsubmit": [
     {
       "name": "CtsPackageInstallTestCases",
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/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index b3ac54a..055afed 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -48,9 +48,12 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 public class BluetoothUtils {
     private static final String TAG = "BluetoothUtils";
@@ -561,6 +564,68 @@
         return isFilterMatched;
     }
 
+    /**
+     * Checks if a given `CachedBluetoothDevice` is available for audio sharing and being switch as
+     * active media device.
+     *
+     * <p>This method determines if the device meets the following criteria to be available:
+     *
+     * <ol>
+     *   <li>Audio sharing session is off.
+     *   <li>The device is one of the two connected devices on the LE Broadcast Assistant profile.
+     *   <li>The device is not currently active on the LE Audio profile.
+     *   <li>There is exactly one other device that is active on the LE Audio profile.
+     * </ol>
+     *
+     * @param cachedDevice The `CachedBluetoothDevice` to check.
+     * @param localBluetoothManager The `LocalBluetoothManager` instance, or null if unavailable.
+     * @return `true` if the device is available for audio sharing and settings as active, `false`
+     *     otherwise.
+     */
+    @WorkerThread
+    public static boolean isAvailableAudioSharingMediaBluetoothDevice(
+            CachedBluetoothDevice cachedDevice,
+            @Nullable LocalBluetoothManager localBluetoothManager) {
+        LocalBluetoothLeBroadcastAssistant assistantProfile =
+                Optional.ofNullable(localBluetoothManager)
+                        .map(LocalBluetoothManager::getProfileManager)
+                        .map(LocalBluetoothProfileManager::getLeAudioBroadcastAssistantProfile)
+                        .orElse(null);
+        LeAudioProfile leAudioProfile =
+                Optional.ofNullable(localBluetoothManager)
+                        .map(LocalBluetoothManager::getProfileManager)
+                        .map(LocalBluetoothProfileManager::getLeAudioProfile)
+                        .orElse(null);
+        CachedBluetoothDeviceManager deviceManager =
+                Optional.ofNullable(localBluetoothManager)
+                        .map(LocalBluetoothManager::getCachedDeviceManager)
+                        .orElse(null);
+        // If any of the profiles are null, or broadcast is already on, return false
+        if (assistantProfile == null
+                || leAudioProfile == null
+                || deviceManager == null
+                || isBroadcasting(localBluetoothManager)) {
+            return false;
+        }
+        Set<Integer> connectedGroupIds =
+                assistantProfile.getAllConnectedDevices().stream()
+                        .map(deviceManager::findDevice)
+                        .filter(Objects::nonNull)
+                        .map(CachedBluetoothDevice::getGroupId)
+                        .collect(Collectors.toSet());
+        Set<Integer> activeGroupIds =
+                leAudioProfile.getActiveDevices().stream()
+                        .map(deviceManager::findDevice)
+                        .filter(Objects::nonNull)
+                        .map(CachedBluetoothDevice::getGroupId)
+                        .collect(Collectors.toSet());
+        int groupId = cachedDevice.getGroupId();
+        return activeGroupIds.size() == 1
+                && !activeGroupIds.contains(groupId)
+                && connectedGroupIds.size() == 2
+                && connectedGroupIds.contains(groupId);
+    }
+
     /** Returns if the le audio sharing is enabled. */
     public static boolean isAudioSharingEnabled() {
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
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/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 926d3cb..3a7b0c7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.settingslib.bluetooth;
 
+import static com.android.settingslib.bluetooth.BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice;
 import static com.android.settingslib.flags.Flags.FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -80,6 +81,7 @@
     @Mock private LocalBluetoothProfileManager mProfileManager;
     @Mock private LocalBluetoothManager mLocalBluetoothManager;
     @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private CachedBluetoothDeviceManager mDeviceManager;
     @Mock private BluetoothLeBroadcastReceiveState mLeBroadcastReceiveState;
 
     private Context mContext;
@@ -104,8 +106,10 @@
         mContext = spy(RuntimeEnvironment.application);
         mSetFlagsRule.disableFlags(FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA);
         when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
+        when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
         when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
         when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
         when(mA2dpProfile.getProfileId()).thenReturn(BluetoothProfile.A2DP);
         when(mLeAudioProfile.getProfileId()).thenReturn(BluetoothProfile.LE_AUDIO);
         when(mHearingAid.getProfileId()).thenReturn(BluetoothProfile.HEARING_AID);
@@ -767,6 +771,116 @@
     }
 
     @Test
+    public void testIsAvailableAudioSharingMediaBluetoothDevice_nullProfiles() {
+        when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(null);
+        boolean result =
+                isAvailableAudioSharingMediaBluetoothDevice(
+                        mCachedBluetoothDevice, mLocalBluetoothManager);
+
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void testIsAvailableAudioSharingMediaBluetoothDevice_alreadyBroadcasting() {
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+
+        boolean result =
+                isAvailableAudioSharingMediaBluetoothDevice(
+                        mCachedBluetoothDevice, mLocalBluetoothManager);
+
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void testIsAvailableAudioSharingMediaBluetoothDevice_availableDevice() {
+        when(mCachedBluetoothDevice.getGroupId()).thenReturn(1);
+        CachedBluetoothDevice cachedBluetoothDevice2 = mock(CachedBluetoothDevice.class);
+        when(cachedBluetoothDevice2.getGroupId()).thenReturn(2);
+
+        BluetoothDevice device1 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device1)).thenReturn(mCachedBluetoothDevice);
+        BluetoothDevice device2 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device2)).thenReturn(cachedBluetoothDevice2);
+
+        when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1, device2));
+        when(mLeAudioProfile.getActiveDevices()).thenReturn(ImmutableList.of(device1));
+
+        boolean result =
+                isAvailableAudioSharingMediaBluetoothDevice(
+                        cachedBluetoothDevice2, mLocalBluetoothManager);
+
+        assertThat(result).isTrue();
+    }
+
+    @Test
+    public void testIsAvailableAudioSharingMediaBluetoothDevice_alreadyActive() {
+        when(mCachedBluetoothDevice.getGroupId()).thenReturn(1);
+        CachedBluetoothDevice cachedBluetoothDevice2 = mock(CachedBluetoothDevice.class);
+        when(cachedBluetoothDevice2.getGroupId()).thenReturn(2);
+
+        BluetoothDevice device1 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device1)).thenReturn(mCachedBluetoothDevice);
+        BluetoothDevice device2 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device2)).thenReturn(cachedBluetoothDevice2);
+
+        when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1, device2));
+        when(mLeAudioProfile.getActiveDevices()).thenReturn(ImmutableList.of(device1));
+
+        boolean result =
+                isAvailableAudioSharingMediaBluetoothDevice(
+                        mCachedBluetoothDevice, mLocalBluetoothManager);
+
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void testIsAvailableAudioSharingMediaBluetoothDevice_notConnected() {
+        when(mCachedBluetoothDevice.getGroupId()).thenReturn(1);
+        CachedBluetoothDevice cachedBluetoothDevice2 = mock(CachedBluetoothDevice.class);
+        when(cachedBluetoothDevice2.getGroupId()).thenReturn(2);
+
+        BluetoothDevice device1 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device1)).thenReturn(mCachedBluetoothDevice);
+        BluetoothDevice device2 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device2)).thenReturn(cachedBluetoothDevice2);
+
+        when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device2));
+        when(mLeAudioProfile.getActiveDevices()).thenReturn(ImmutableList.of(device1));
+
+        boolean result =
+                isAvailableAudioSharingMediaBluetoothDevice(
+                        mCachedBluetoothDevice, mLocalBluetoothManager);
+
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void testIsAvailableAudioSharingMediaBluetoothDevice_moreThanTwoConnected() {
+        when(mCachedBluetoothDevice.getGroupId()).thenReturn(1);
+        CachedBluetoothDevice cachedBluetoothDevice2 = mock(CachedBluetoothDevice.class);
+        when(cachedBluetoothDevice2.getGroupId()).thenReturn(2);
+        CachedBluetoothDevice cachedBluetoothDevice3 = mock(CachedBluetoothDevice.class);
+        when(cachedBluetoothDevice3.getGroupId()).thenReturn(3);
+
+        BluetoothDevice device1 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device1)).thenReturn(mCachedBluetoothDevice);
+        BluetoothDevice device2 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device2)).thenReturn(cachedBluetoothDevice2);
+        BluetoothDevice device3 = mock(BluetoothDevice.class);
+        when(mDeviceManager.findDevice(device3)).thenReturn(cachedBluetoothDevice3);
+
+        when(mAssistant.getAllConnectedDevices())
+                .thenReturn(ImmutableList.of(device1, device2, device3));
+        when(mLeAudioProfile.getActiveDevices()).thenReturn(ImmutableList.of(device1));
+
+        boolean result =
+                isAvailableAudioSharingMediaBluetoothDevice(
+                        cachedBluetoothDevice2, mLocalBluetoothManager);
+
+        assertThat(result).isFalse();
+    }
+
+    @Test
     public void getAudioDeviceAttributesForSpatialAudio_bleHeadset() {
         String address = "11:22:33:44:55:66";
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
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/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 398c915..2e98c1f 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -376,6 +376,10 @@
     <!-- Listen to (dis-)connection of external displays and enable / disable them. -->
     <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
 
+    <!-- To be able to intercept meta key events, might need to be removed once b/358569822
+         is ready -->
+    <uses-permission android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
+
     <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
     <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
     <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 476fd8b..7c0db8d 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1066,6 +1066,13 @@
 }
 
 flag {
+  name: "media_controls_drawables_reuse"
+  namespace: "systemui"
+  description: "Re-use created media drawables for media controls"
+  bug: "358402034"
+}
+
+flag {
   namespace: "systemui"
   name: "enable_view_capture_tracing"
   description: "Enables view capture tracing in System UI."
@@ -1338,4 +1345,4 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index d15bda0..853dc6f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -60,6 +60,7 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -631,7 +632,11 @@
                     modifier =
                         Modifier.weight(1f)
                             .fillMaxHeight()
-                            .padding(end = screenCornerRadius / 2f, bottom = navBarBottomHeight)
+                            .padding(
+                                end =
+                                    dimensionResource(R.dimen.notification_panel_margin_horizontal),
+                                bottom = navBarBottomHeight
+                            )
                             .then(brightnessMirrorShowingModifier)
                 )
             }
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/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 9792c28..1bc2e24 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -60,6 +60,7 @@
 import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.whenever
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancelChildren
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -2135,6 +2136,14 @@
     @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
     fun glanceableHubToOccluded_communalKtfRefactor() =
         testScope.runTest {
+            // GIVEN device is not dreaming
+            powerInteractor.setAwakeForTest()
+            keyguardRepository.setDreaming(false)
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            advanceTimeBy(600.milliseconds)
+
             // GIVEN a prior transition has run to GLANCEABLE_HUB
             communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
             runCurrent()
@@ -2298,6 +2307,54 @@
             coroutineContext.cancelChildren()
         }
 
+    @Test
+    @BrokenWithSceneContainer(339465026)
+    @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+    fun glanceableHubToOccludedDoesNotTriggerWhenDreamStateChanges_communalKtfRefactor() =
+        testScope.runTest {
+            // GIVEN that we are dreaming and not dozing
+            powerInteractor.setAwakeForTest()
+            keyguardRepository.setDreaming(true)
+            keyguardRepository.setKeyguardOccluded(true)
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            advanceTimeBy(700.milliseconds)
+            clearInvocations(transitionRepository)
+
+            // GIVEN a prior transition has run to GLANCEABLE_HUB
+            communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
+            runCurrent()
+            assertThat(transitionRepository)
+                .startedTransition(
+                    ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+                    from = KeyguardState.DREAMING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                )
+            clearInvocations(transitionRepository)
+
+            // WHEN dream ends but we are still occluded
+            keyguardRepository.setDreaming(false)
+            runCurrent()
+            assertThat(transitionRepository).noTransitionsStarted()
+
+            // Simulate occlusion signal changing due to dream terminating and then occluding again
+            // due to a new activity starting a couple milliseconds later.
+            keyguardRepository.setKeyguardOccluded(false)
+            advanceTimeBy(10.milliseconds)
+            keyguardRepository.setKeyguardOccluded(true)
+            advanceTimeBy(200.milliseconds)
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.OCCLUDED,
+                )
+
+            coroutineContext.cancelChildren()
+        }
+
     private suspend fun TestScope.runTransitionAndSetWakefulness(
         from: KeyguardState,
         to: KeyguardState
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt
new file mode 100644
index 0000000..f6f58c9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.lifecycle
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BaseActivatableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val underTest = FakeActivatable()
+
+    @Test
+    fun activate() =
+        testScope.runTest {
+            assertThat(underTest.isActive).isFalse()
+            assertThat(underTest.activationCount).isEqualTo(0)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            underTest.activateIn(testScope)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+        }
+
+    @Test
+    fun activate_andCancel() =
+        testScope.runTest {
+            assertThat(underTest.isActive).isFalse()
+            assertThat(underTest.activationCount).isEqualTo(0)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            val job = Job()
+            underTest.activateIn(testScope, context = job)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            job.cancel()
+            runCurrent()
+            assertThat(underTest.isActive).isFalse()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(1)
+        }
+
+    @Test
+    fun activate_afterCancellation() =
+        testScope.runTest {
+            assertThat(underTest.isActive).isFalse()
+            assertThat(underTest.activationCount).isEqualTo(0)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            val job = Job()
+            underTest.activateIn(testScope, context = job)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            job.cancel()
+            runCurrent()
+            assertThat(underTest.isActive).isFalse()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(1)
+
+            underTest.activateIn(testScope)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(underTest.activationCount).isEqualTo(2)
+            assertThat(underTest.cancellationCount).isEqualTo(1)
+        }
+
+    @Test(expected = IllegalStateException::class)
+    fun activate_whileActive_throws() =
+        testScope.runTest {
+            assertThat(underTest.isActive).isFalse()
+            assertThat(underTest.activationCount).isEqualTo(0)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            underTest.activateIn(testScope)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            underTest.activateIn(testScope)
+            runCurrent()
+        }
+
+    @Test
+    fun addChild_beforeActive_activatesChildrenOnceActivated() =
+        testScope.runTest {
+            val child1 = FakeActivatable()
+            val child2 = FakeActivatable()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            assertThat(underTest.isActive).isFalse()
+            underTest.addChild(child1)
+            underTest.addChild(child2)
+            assertThat(underTest.isActive).isFalse()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            underTest.activateIn(this)
+            runCurrent()
+
+            assertThat(underTest.isActive).isTrue()
+            assertThat(child1.isActive).isTrue()
+            assertThat(child2.isActive).isTrue()
+        }
+
+    @Test
+    fun addChild_whileActive_activatesChildrenImmediately() =
+        testScope.runTest {
+            underTest.activateIn(this)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+
+            val child1 = FakeActivatable()
+            val child2 = FakeActivatable()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            underTest.addChild(child1)
+            underTest.addChild(child2)
+            runCurrent()
+
+            assertThat(child1.isActive).isTrue()
+            assertThat(child2.isActive).isTrue()
+        }
+
+    @Test
+    fun addChild_afterCancellation_doesNotActivateChildren() =
+        testScope.runTest {
+            val job = Job()
+            underTest.activateIn(this, context = job)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            job.cancel()
+            runCurrent()
+            assertThat(underTest.isActive).isFalse()
+
+            val child1 = FakeActivatable()
+            val child2 = FakeActivatable()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            underTest.addChild(child1)
+            underTest.addChild(child2)
+            runCurrent()
+
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+        }
+
+    @Test
+    fun activate_cancellation_cancelsCurrentChildren() =
+        testScope.runTest {
+            val job = Job()
+            underTest.activateIn(this, context = job)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+
+            val child1 = FakeActivatable()
+            val child2 = FakeActivatable()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            underTest.addChild(child1)
+            underTest.addChild(child2)
+            runCurrent()
+
+            assertThat(child1.isActive).isTrue()
+            assertThat(child2.isActive).isTrue()
+
+            job.cancel()
+            runCurrent()
+            assertThat(underTest.isActive).isFalse()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+        }
+
+    @Test
+    fun activate_afterCancellation_reactivatesCurrentChildren() =
+        testScope.runTest {
+            val job = Job()
+            underTest.activateIn(this, context = job)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+
+            val child1 = FakeActivatable()
+            val child2 = FakeActivatable()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            underTest.addChild(child1)
+            underTest.addChild(child2)
+            runCurrent()
+
+            assertThat(child1.isActive).isTrue()
+            assertThat(child2.isActive).isTrue()
+
+            job.cancel()
+            runCurrent()
+            assertThat(underTest.isActive).isFalse()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            underTest.activateIn(this)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(child1.isActive).isTrue()
+            assertThat(child2.isActive).isTrue()
+        }
+
+    @Test
+    fun removeChild_beforeActive_neverActivatesChild() =
+        testScope.runTest {
+            val child1 = FakeActivatable()
+            val child2 = FakeActivatable()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            assertThat(underTest.isActive).isFalse()
+            underTest.addChild(child1)
+            underTest.addChild(child2)
+            assertThat(underTest.isActive).isFalse()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+        }
+
+    @Test
+    fun removeChild_whileActive_cancelsChild() =
+        testScope.runTest {
+            val child1 = FakeActivatable()
+            val child2 = FakeActivatable()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            assertThat(underTest.isActive).isFalse()
+            underTest.addChild(child1)
+            underTest.addChild(child2)
+            assertThat(underTest.isActive).isFalse()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            underTest.activateIn(this)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(child1.isActive).isTrue()
+            assertThat(child2.isActive).isTrue()
+
+            underTest.removeChild(child1)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isTrue()
+        }
+
+    @Test
+    fun removeChild_afterCancellation_doesNotReactivateChildren() =
+        testScope.runTest {
+            val child1 = FakeActivatable()
+            val child2 = FakeActivatable()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            assertThat(underTest.isActive).isFalse()
+            underTest.addChild(child1)
+            underTest.addChild(child2)
+            assertThat(underTest.isActive).isFalse()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            val job = Job()
+            underTest.activateIn(this, context = job)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(child1.isActive).isTrue()
+            assertThat(child2.isActive).isTrue()
+
+            job.cancel()
+            runCurrent()
+            assertThat(underTest.isActive).isFalse()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isFalse()
+
+            underTest.removeChild(child1)
+            underTest.activateIn(this)
+            runCurrent()
+            assertThat(underTest.isActive).isTrue()
+            assertThat(child1.isActive).isFalse()
+            assertThat(child2.isActive).isTrue()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SafeActivatableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SafeActivatableTest.kt
deleted file mode 100644
index 9484821..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SafeActivatableTest.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.lifecycle
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class SafeActivatableTest : SysuiTestCase() {
-
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
-
-    private val underTest = FakeActivatable()
-
-    @Test
-    fun activate() =
-        testScope.runTest {
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(0)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            underTest.activateIn(testScope)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-        }
-
-    @Test
-    fun activate_andCancel() =
-        testScope.runTest {
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(0)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            val job = Job()
-            underTest.activateIn(testScope, context = job)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            job.cancel()
-            runCurrent()
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(1)
-        }
-
-    @Test
-    fun activate_afterCancellation() =
-        testScope.runTest {
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(0)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            val job = Job()
-            underTest.activateIn(testScope, context = job)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            job.cancel()
-            runCurrent()
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(1)
-
-            underTest.activateIn(testScope)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(2)
-            assertThat(underTest.cancellationCount).isEqualTo(1)
-        }
-
-    @Test(expected = IllegalStateException::class)
-    fun activate_whileActive_throws() =
-        testScope.runTest {
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(0)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            underTest.activateIn(testScope)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            underTest.activateIn(testScope)
-            runCurrent()
-        }
-}
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/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index bbb467f..64a13de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -28,9 +28,7 @@
 import com.android.internal.logging.uiEventLoggerFake
 import com.android.internal.policy.IKeyguardDismissCallback
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
-import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
@@ -357,6 +355,7 @@
             kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
                 SuccessFingerprintAuthenticationStatus(0, true)
             )
+            runCurrent()
 
             assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings)
             assertThat(alternateBouncerVisible).isFalse()
@@ -507,6 +506,33 @@
         }
 
     @Test
+    fun hideAlternateBouncerAndNotifyDismissCancelledWhenDeviceSleeps() =
+        testScope.runTest {
+            val alternateBouncerVisible by
+                collectLastValue(bouncerRepository.alternateBouncerVisible)
+            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
+            prepareState(
+                isDeviceUnlocked = false,
+                initialSceneKey = Scenes.Shade,
+            )
+            assertThat(currentSceneKey).isEqualTo(Scenes.Shade)
+            bouncerRepository.setAlternateVisible(true)
+            underTest.start()
+
+            // run all pending dismiss succeeded/cancelled calls from setup:
+            kosmos.fakeExecutor.runAllReady()
+
+            val dismissCallback: IKeyguardDismissCallback = mock()
+            kosmos.dismissCallbackRegistry.addCallback(dismissCallback)
+            powerInteractor.setAsleepForTest()
+            runCurrent()
+            kosmos.fakeExecutor.runAllReady()
+
+            assertThat(alternateBouncerVisible).isFalse()
+            verify(dismissCallback).onDismissCancelled()
+        }
+
+    @Test
     fun switchToLockscreenWhenDeviceSleepsLocked() =
         testScope.runTest {
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -1644,19 +1670,27 @@
         }
 
     @Test
-    fun notifyKeyguardDismissCallbacks_whenUnlocking_onDismissSucceeded() =
+    fun notifyKeyguardDismissCallbacks_whenUnlockingFromBouncer_onDismissSucceeded() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.currentScene)
-            prepareState()
+            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
+            prepareState(
+                authenticationMethod = AuthenticationMethodModel.Pin,
+                isDeviceUnlocked = false,
+                initialSceneKey = Scenes.Bouncer,
+            )
+            assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer)
             underTest.start()
+
+            // run all pending dismiss succeeded/cancelled calls from setup:
+            kosmos.fakeExecutor.runAllReady()
+
             val dismissCallback: IKeyguardDismissCallback = mock()
             kosmos.dismissCallbackRegistry.addCallback(dismissCallback)
 
-            // Switch to bouncer and unlock device:
-            sceneInteractor.changeScene(Scenes.Bouncer, "")
-            assertThat(currentScene).isEqualTo(Scenes.Bouncer)
-            kosmos.authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
-            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
             kosmos.fakeExecutor.runAllReady()
 
             verify(dismissCallback).onDismissSucceeded()
@@ -1665,19 +1699,26 @@
     @Test
     fun notifyKeyguardDismissCallbacks_whenLeavingBouncer_onDismissCancelled() =
         testScope.runTest {
+            val isUnlocked by collectLastValue(kosmos.deviceEntryInteractor.isUnlocked)
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             prepareState()
             underTest.start()
+
+            // run all pending dismiss succeeded/cancelled calls from setup:
+            kosmos.fakeExecutor.runAllReady()
+
             val dismissCallback: IKeyguardDismissCallback = mock()
             kosmos.dismissCallbackRegistry.addCallback(dismissCallback)
 
             // Switch to bouncer:
             sceneInteractor.changeScene(Scenes.Bouncer, "")
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
+            runCurrent()
 
-            // Return to lockscreen:
+            // Return to lockscreen when isUnlocked=false:
             sceneInteractor.changeScene(Scenes.Lockscreen, "")
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(isUnlocked).isFalse()
             runCurrent()
             kosmos.fakeExecutor.runAllReady()
 
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 8a2e767a..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/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 3019fe7..ed9ba7a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -35,9 +35,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
-import android.graphics.Rect;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -45,9 +43,6 @@
 import android.provider.Settings;
 import android.util.Log;
 import android.view.Display;
-import android.view.IRecentsAnimationController;
-import android.view.IRecentsAnimationRunner;
-import android.view.RemoteAnimationTarget;
 import android.window.TaskSnapshot;
 
 import com.android.internal.app.IVoiceInteractionManagerService;
@@ -55,7 +50,6 @@
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
 import java.util.List;
-import java.util.function.Consumer;
 
 public class ActivityManagerWrapper {
 
@@ -190,69 +184,13 @@
     }
 
     /**
-     * Starts the recents activity. The caller should manage the thread on which this is called.
+     * Preloads the recents activity. The caller should manage the thread on which this is called.
      */
-    public void startRecentsActivity(Intent intent, long eventTime,
-            final RecentsAnimationListener animationHandler, final Consumer<Boolean> resultCallback,
-            Handler resultCallbackHandler) {
-        boolean result = startRecentsActivity(intent, eventTime, animationHandler);
-        if (resultCallback != null && resultCallbackHandler != null) {
-            resultCallbackHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    resultCallback.accept(result);
-                }
-            });
-        }
-    }
-
-    /**
-     * Starts the recents activity. The caller should manage the thread on which this is called.
-     */
-    public boolean startRecentsActivity(
-            Intent intent, long eventTime, RecentsAnimationListener animationHandler) {
+    public void preloadRecentsActivity(Intent intent) {
         try {
-            IRecentsAnimationRunner runner = null;
-            if (animationHandler != null) {
-                runner = new IRecentsAnimationRunner.Stub() {
-                    @Override
-                    public void onAnimationStart(IRecentsAnimationController controller,
-                            RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
-                            Rect homeContentInsets, Rect minimizedHomeBounds,
-                            Bundle extras) {
-                        final RecentsAnimationControllerCompat controllerCompat =
-                                new RecentsAnimationControllerCompat(controller);
-                        animationHandler.onAnimationStart(controllerCompat, apps,
-                                wallpapers, homeContentInsets, minimizedHomeBounds, extras);
-                    }
-
-                    @Override
-                    public void onAnimationCanceled(int[] taskIds, TaskSnapshot[] taskSnapshots) {
-                        animationHandler.onAnimationCanceled(
-                                ThumbnailData.wrap(taskIds, taskSnapshots));
-                    }
-
-                    @Override
-                    public void onTasksAppeared(RemoteAnimationTarget[] apps) {
-                        animationHandler.onTasksAppeared(apps);
-                    }
-                };
-            }
-            getService().startRecentsActivity(intent, eventTime, runner);
-            return true;
+            getService().preloadRecentsActivity(intent);
         } catch (Exception e) {
-            return false;
-        }
-    }
-
-    /**
-     * Cancels the remote recents animation started from {@link #startRecentsActivity}.
-     */
-    public void cancelRecentsAnimation(boolean restoreHomeRootTaskPosition) {
-        try {
-            getService().cancelRecentsAnimation(restoreHomeRootTaskPosition);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to cancel recents animation", e);
+            Log.w(TAG, "Failed to preload recents activity", e);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
index 6c46318..f8b445b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
@@ -100,17 +100,20 @@
         private final WindowMagnifierCallback mWindowMagnifierCallback;
         private final SysUiState mSysUiState;
         private final SecureSettings mSecureSettings;
+        private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
 
         WindowMagnificationControllerSupplier(Context context, Handler handler,
                 WindowMagnifierCallback windowMagnifierCallback,
                 DisplayManager displayManager, SysUiState sysUiState,
-                SecureSettings secureSettings) {
+                SecureSettings secureSettings,
+                ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) {
             super(displayManager);
             mContext = context;
             mHandler = handler;
             mWindowMagnifierCallback = windowMagnifierCallback;
             mSysUiState = sysUiState;
             mSecureSettings = secureSettings;
+            mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager;
         }
 
         @Override
@@ -137,7 +140,8 @@
                     mSecureSettings,
                     scvhSupplier,
                     new SfVsyncFrameCallbackProvider(),
-                    WindowManagerGlobal::getWindowSession);
+                    WindowManagerGlobal::getWindowSession,
+                    mViewCaptureAwareWindowManager);
         }
     }
 
@@ -267,7 +271,7 @@
         mA11yLogger = a11yLogger;
         mWindowMagnificationControllerSupplier = new WindowMagnificationControllerSupplier(context,
                 mHandler, mWindowMagnifierCallback,
-                displayManager, sysUiState, secureSettings);
+                displayManager, sysUiState, secureSettings, viewCaptureAwareWindowManager);
         mFullscreenMagnificationControllerSupplier = new FullscreenMagnificationControllerSupplier(
                 context, displayManager, mHandler, mExecutor, iWindowManager);
         mMagnificationSettingsSupplier = new SettingsSupplier(context,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index f0483a5..0883a06 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -80,6 +80,7 @@
 import androidx.annotation.UiThread;
 import androidx.core.math.MathUtils;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@@ -126,6 +127,7 @@
     private final SurfaceControl.Transaction mTransaction;
 
     private final WindowManager mWm;
+    private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
 
     private float mScale;
     private int mSettingsButtonIndex = MagnificationSize.DEFAULT;
@@ -258,7 +260,8 @@
             SecureSettings secureSettings,
             Supplier<SurfaceControlViewHost> scvhSupplier,
             SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
-            Supplier<IWindowSession> globalWindowSessionSupplier) {
+            Supplier<IWindowSession> globalWindowSessionSupplier,
+            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) {
         mContext = context;
         mHandler = handler;
         mAnimationController = animationController;
@@ -280,6 +283,7 @@
 
         mWm = context.getSystemService(WindowManager.class);
         mWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds());
+        mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager;
 
         mResources = mContext.getResources();
         mScale = secureSettings.getFloatForUser(
@@ -510,7 +514,7 @@
             mHandler.removeCallbacks(mMirrorViewRunnable);
             mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
             if (!Flags.createWindowlessWindowMagnifier()) {
-                mWm.removeView(mMirrorView);
+                mViewCaptureAwareWindowManager.removeView(mMirrorView);
             }
             mMirrorView = null;
         }
@@ -722,7 +726,7 @@
             return v.onApplyWindowInsets(insets);
         });
 
-        mWm.addView(mMirrorView, params);
+        mViewCaptureAwareWindowManager.addView(mMirrorView, params);
 
         SurfaceHolder holder = mMirrorSurfaceView.getHolder();
         holder.addCallback(this);
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/charging/WirelessChargingAnimation.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
index 3808ab7..e5e9c46 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
@@ -28,6 +28,7 @@
 import android.view.Gravity;
 import android.view.WindowManager;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
@@ -59,10 +60,11 @@
      */
     private WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper,
             int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing,
-            RippleShape rippleShape, UiEventLogger uiEventLogger) {
+            RippleShape rippleShape, UiEventLogger uiEventLogger,
+            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) {
         mCurrentWirelessChargingView = new WirelessChargingView(context, looper,
                 transmittingBatteryLevel, batteryLevel, callback, isDozing,
-                rippleShape, uiEventLogger);
+                rippleShape, uiEventLogger, viewCaptureAwareWindowManager);
     }
 
     /**
@@ -73,9 +75,11 @@
     public static WirelessChargingAnimation makeWirelessChargingAnimation(@NonNull Context context,
             @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel,
             Callback callback, boolean isDozing, RippleShape rippleShape,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) {
         return new WirelessChargingAnimation(context, looper, transmittingBatteryLevel,
-                batteryLevel, callback, isDozing, rippleShape, uiEventLogger);
+                batteryLevel, callback, isDozing, rippleShape, uiEventLogger,
+                viewCaptureAwareWindowManager);
     }
 
     /**
@@ -83,10 +87,11 @@
      * battery level without charging number shown.
      */
     public static WirelessChargingAnimation makeChargingAnimationWithNoBatteryLevel(
-            @NonNull Context context, RippleShape rippleShape, UiEventLogger uiEventLogger) {
+            @NonNull Context context, RippleShape rippleShape, UiEventLogger uiEventLogger,
+            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) {
         return makeWirelessChargingAnimation(context, null,
                 UNKNOWN_BATTERY_LEVEL, UNKNOWN_BATTERY_LEVEL, null, false,
-                rippleShape, uiEventLogger);
+                rippleShape, uiEventLogger, viewCaptureAwareWindowManager);
     }
 
     /**
@@ -118,17 +123,19 @@
         private int mGravity;
         private WirelessChargingLayout mView;
         private WirelessChargingLayout mNextView;
-        private WindowManager mWM;
+        private ViewCaptureAwareWindowManager mWM;
         private Callback mCallback;
 
         public WirelessChargingView(Context context, @Nullable Looper looper,
                 int transmittingBatteryLevel, int batteryLevel, Callback callback,
-                boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger) {
+                boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger,
+                ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) {
             mCallback = callback;
             mNextView = new WirelessChargingLayout(context, transmittingBatteryLevel, batteryLevel,
                     isDozing, rippleShape);
             mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER;
             mUiEventLogger = uiEventLogger;
+            mWM = viewCaptureAwareWindowManager;
 
             final WindowManager.LayoutParams params = mParams;
             params.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -200,7 +207,6 @@
                 if (context == null) {
                     context = mView.getContext();
                 }
-                mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
                 mParams.packageName = packageName;
                 mParams.hideTimeoutMilliseconds = DURATION;
 
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/inputdevice/tutorial/InputDeviceTutorialLogger.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt
new file mode 100644
index 0000000..9525174
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.InputDeviceTutorialLog
+import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "InputDeviceTutorial"
+
+class InputDeviceTutorialLogger
+@Inject
+constructor(@InputDeviceTutorialLog private val buffer: LogBuffer) {
+
+    fun log(@CompileTimeConstant s: String) {
+        buffer.log(TAG, LogLevel.INFO, message = s)
+    }
+
+    fun logGoingToScreen(screen: Screen, context: TutorialContext) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                str1 = screen.toString()
+                str2 = context.string
+            },
+            { "Emitting new screen $str1 in $str2" }
+        )
+    }
+
+    fun logCloseTutorial(context: TutorialContext) {
+        buffer.log(TAG, LogLevel.INFO, { str1 = context.string }, { "Closing $str1" })
+    }
+
+    enum class TutorialContext(val string: String) {
+        KEYBOARD_TOUCHPAD_TUTORIAL("keyboard touchpad tutorial"),
+        TOUCHPAD_TUTORIAL("touchpad tutorial"),
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
index c5b0ca7..6bc640d 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
@@ -17,15 +17,19 @@
 package com.android.systemui.inputdevice.tutorial.ui.composable
 
 import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyEventType
@@ -46,18 +50,27 @@
     BackHandler(onBack = onBack)
     val screenConfig = buildScreenConfig()
     var actionState by remember { mutableStateOf(NOT_STARTED) }
+    val focusRequester = remember { FocusRequester() }
     Box(
         modifier =
-            Modifier.fillMaxSize().onKeyEvent { keyEvent: KeyEvent ->
-                // temporary before we can access Action/Meta key
-                if (keyEvent.key == Key.AltLeft && keyEvent.type == KeyEventType.KeyUp) {
-                    actionState = FINISHED
+            Modifier.fillMaxSize()
+                .onKeyEvent { keyEvent: KeyEvent ->
+                    if (keyEvent.key == Key.MetaLeft && keyEvent.type == KeyEventType.KeyUp) {
+                        actionState = FINISHED
+                    }
+                    true
                 }
-                true
-            }
+                .focusRequester(focusRequester)
+                .focusable()
     ) {
         ActionTutorialContent(actionState, onDoneButtonClicked, screenConfig)
     }
+    LaunchedEffect(Unit) {
+        // we need to request focus on main container so it can handle all key events immediately
+        // when it's open. Otherwise user needs to press non-modifier key before modifier key can
+        // be handled as nothing is focused
+        focusRequester.requestFocus()
+    }
 }
 
 @Composable
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
index 34ecc95..8debe79 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
@@ -68,6 +68,7 @@
         enableEdgeToEdge()
         // required to handle 3+ fingers on touchpad
         window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+        window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS)
         lifecycle.addObserver(vm)
         lifecycleScope.launch {
             vm.closeActivity.collect { finish ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 0aa50e0..199caa1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -37,16 +37,21 @@
 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.noneOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
+@OptIn(FlowPreview::class)
 @SysUISingleton
 class FromGlanceableHubTransitionInteractor
 @Inject
@@ -196,19 +201,26 @@
             }
         } else if (communalSceneKtfRefactor()) {
             scope.launch {
-                allOf(
+                combine(
                         keyguardInteractor.isKeyguardOccluded,
-                        noneOf(
-                            // Dream is a special-case of occluded, so filter out the dreaming
-                            // case here.
-                            keyguardInteractor.isDreaming,
-                            // When launching activities from widgets on the hub, we have a
-                            // custom occlusion animation.
-                            communalSceneInteractor.isLaunchingWidget,
-                        ),
+                        keyguardInteractor.isAbleToDream
+                            // Debounce the dreaming signal since there is a race condition between
+                            // the occluded and dreaming signals. We therefore add a small delay
+                            // to give enough time for occluded to flip to false when the dream
+                            // ends, to avoid transitioning to OCCLUDED erroneously when exiting
+                            // the dream.
+                            .debounce(100.milliseconds),
+                        ::Pair
                     )
-                    .filterRelevantKeyguardStateAnd { isOccludedAndNotDreamingNorLaunchingWidget ->
-                        isOccludedAndNotDreamingNorLaunchingWidget
+                    .sampleFilter(
+                        // When launching activities from widgets on the hub, we have a
+                        // custom occlusion animation.
+                        communalSceneInteractor.isLaunchingWidget,
+                    ) { launchingWidget ->
+                        !launchingWidget
+                    }
+                    .filterRelevantKeyguardStateAnd { (isOccluded, isDreaming) ->
+                        isOccluded && !isDreaming
                     }
                     .collect { _ ->
                         communalSceneInteractor.changeScene(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
index 1c445a7..7801c00 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
@@ -17,15 +17,16 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.shared.model.DismissAction
 import com.android.systemui.keyguard.shared.model.KeyguardDone
-import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.domain.resolver.NotifShadeSceneFamilyResolver
 import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolver
@@ -61,6 +62,8 @@
     deviceEntryInteractor: DeviceEntryInteractor,
     quickSettingsSceneFamilyResolver: QuickSettingsSceneFamilyResolver,
     notifShadeSceneFamilyResolver: NotifShadeSceneFamilyResolver,
+    powerInteractor: PowerInteractor,
+    alternateBouncerInteractor: AlternateBouncerInteractor,
 ) {
     val dismissAction: Flow<DismissAction> = repository.dismissAction
 
@@ -124,10 +127,12 @@
                     scene = Scenes.Bouncer,
                     stateWithoutSceneContainer = PRIMARY_BOUNCER
                 ),
-                transitionInteractor.isFinishedIn(state = ALTERNATE_BOUNCER),
+                alternateBouncerInteractor.isVisible,
                 isOnShadeWhileUnlocked,
-            ) { isOnGone, isOnBouncer, isOnAltBouncer, isOnShadeWhileUnlocked ->
-                !isOnGone && !isOnBouncer && !isOnAltBouncer && !isOnShadeWhileUnlocked
+                powerInteractor.isAsleep,
+            ) { isOnGone, isOnBouncer, isOnAltBouncer, isOnShadeWhileUnlocked, isAsleep ->
+                (!isOnGone && !isOnBouncer && !isOnAltBouncer && !isOnShadeWhileUnlocked) ||
+                    isAsleep
             }
             .filter { it }
             .sampleFilter(dismissAction) { it !is DismissAction.None }
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt
new file mode 100644
index 0000000..03476ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.lifecycle
+
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+/**
+ * A base [Activatable] with the following characteristics:
+ * 1. **Can be concurrently activated by no more than one owner.** A previous call to [activate]
+ *    must be canceled before a new call to [activate] can be made. Trying to call [activate] while
+ *    already active will fail with an error
+ * 2. **Can manage child [Activatable]s**. See [addChild] and [removeChild]. Added children
+ *    automatically track the activation state of the parent such that when the parent is active,
+ *    the children are active and vice-versa. Children are also retained such that deactivating the
+ *    parent and reactivating it also cancels and reactivates the children.
+ */
+abstract class BaseActivatable : Activatable {
+
+    private val _isActive = AtomicBoolean(false)
+
+    var isActive: Boolean
+        get() = _isActive.get()
+        private set(value) {
+            _isActive.set(value)
+        }
+
+    final override suspend fun activate(): Nothing {
+        val allowed = _isActive.compareAndSet(false, true)
+        check(allowed) { "Cannot activate an already active activatable!" }
+
+        coroutineScope {
+            try {
+                launch { manageChildren() }
+                onActivated()
+            } finally {
+                isActive = false
+            }
+        }
+    }
+
+    /**
+     * Notifies that the [Activatable] has been activated.
+     *
+     * Serves as an entrypoint to kick off coroutine work that the object requires in order to keep
+     * its state fresh and/or perform side-effects.
+     *
+     * The method suspends and doesn't return until all work required by the object is finished. In
+     * most cases, it's expected for the work to remain ongoing forever so this method will forever
+     * suspend its caller until the coroutine that called it is canceled.
+     *
+     * Implementations could follow this pattern:
+     * ```kotlin
+     * override suspend fun onActivated(): Nothing {
+     *     coroutineScope {
+     *         launch { ... }
+     *         launch { ... }
+     *         launch { ... }
+     *     }
+     * }
+     * ```
+     *
+     * @see activate
+     */
+    protected abstract suspend fun onActivated(): Nothing
+
+    private val newChildren = Channel<Activatable>(Channel.BUFFERED)
+    private val jobByChild: MutableMap<Activatable, Job> by lazy { mutableMapOf() }
+
+    private suspend fun manageChildren(): Nothing {
+        coroutineScope {
+            // Reactivate children that were added during a previous activation:
+            jobByChild.keys.forEach { child -> jobByChild[child] = launch { child.activate() } }
+
+            // Process requests to add more children:
+            newChildren.receiveAsFlow().collect { newChild ->
+                removeChildInternal(newChild)
+                jobByChild[newChild] = launch { newChild.activate() }
+            }
+
+            awaitCancellation()
+        }
+    }
+
+    fun addChild(child: Activatable) {
+        newChildren.trySend(child)
+    }
+
+    fun removeChild(child: Activatable) {
+        removeChildInternal(child)
+    }
+
+    private fun removeChildInternal(child: Activatable) {
+        jobByChild.remove(child)?.cancel()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SafeActivatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SafeActivatable.kt
deleted file mode 100644
index 4dd76f8..0000000
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SafeActivatable.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.lifecycle
-
-import java.util.concurrent.atomic.AtomicBoolean
-
-/**
- * An [Activatable] that can be concurrently activated by no more than one owner.
- *
- * A previous call to [activate] must be canceled before a new call to [activate] can be made.
- * Trying to call [activate] while already active will fail with an error.
- */
-abstract class SafeActivatable : Activatable {
-
-    private val _isActive = AtomicBoolean(false)
-
-    var isActive: Boolean
-        get() = _isActive.get()
-        private set(value) {
-            _isActive.set(value)
-        }
-
-    final override suspend fun activate(): Nothing {
-        val allowed = _isActive.compareAndSet(false, true)
-        check(allowed) { "Cannot activate an already active activatable!" }
-
-        try {
-            onActivated()
-        } finally {
-            isActive = false
-        }
-    }
-
-    /**
-     * Notifies that the [Activatable] has been activated.
-     *
-     * Serves as an entrypoint to kick off coroutine work that the object requires in order to keep
-     * its state fresh and/or perform side-effects.
-     *
-     * The method suspends and doesn't return until all work required by the object is finished. In
-     * most cases, it's expected for the work to remain ongoing forever so this method will forever
-     * suspend its caller until the coroutine that called it is canceled.
-     *
-     * Implementations could follow this pattern:
-     * ```kotlin
-     * override suspend fun onActivated(): Nothing {
-     *     coroutineScope {
-     *         launch { ... }
-     *         launch { ... }
-     *         launch { ... }
-     *     }
-     * }
-     * ```
-     *
-     * @see activate
-     */
-    protected abstract suspend fun onActivated(): Nothing
-}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
index 2edde4a..32c4760 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
@@ -18,12 +18,44 @@
 
 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 : SafeActivatable() {
+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/log/dagger/InputDeviceTutorialLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/InputDeviceTutorialLog.kt
new file mode 100644
index 0000000..a788279
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/InputDeviceTutorialLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for input device tutorial. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class InputDeviceTutorialLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 40bb8e1..5cae58a 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -660,6 +660,14 @@
         return factory.create("KeyboardLog", 50);
     }
 
+    /** Provides a {@link LogBuffer} for the input devices tutorial. */
+    @Provides
+    @SysUISingleton
+    @InputDeviceTutorialLog
+    public static LogBuffer provideInputDeviceTutorialLogBuffer(LogBufferFactory factory) {
+        return factory.create("InputDeviceTutorialLog", 50);
+    }
+
     /** Provides a {@link LogBuffer} for {@link PackageChangeRepository} */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt
new file mode 100644
index 0000000..28ee668
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.media.controls.shared
+
+import android.content.Context
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
+import com.android.systemui.Flags.mediaControlsDrawablesReuse
+import com.android.systemui.res.R
+
+object MediaControlDrawables {
+
+    // Play/Pause Button drawables.
+    private var progress: Drawable? = null
+    private var connecting: Drawable? = null
+    private var playIcon: AnimatedVectorDrawable? = null
+    private var playBackground: AnimatedVectorDrawable? = null
+    private var pauseIcon: AnimatedVectorDrawable? = null
+    private var pauseBackground: AnimatedVectorDrawable? = null
+    // Prev button.
+    private var prevIcon: Drawable? = null
+    // Next button.
+    private var nextIcon: Drawable? = null
+    // Output switcher drawables.
+    private var leAudioSharing: Drawable? = null
+    private var antenna: Drawable? = null
+    private var groupDevice: Drawable? = null
+    private var homeDevices: Drawable? = null
+    // Guts drawables.
+    private var outline: Drawable? = null
+    private var solid: Drawable? = null
+
+    fun getProgress(context: Context): Drawable? {
+        return progress
+            ?: context.getDrawable(com.android.internal.R.drawable.progress_small_material).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                progress = it
+            }
+    }
+
+    fun getConnecting(context: Context): Drawable? {
+        return connecting
+            ?: context.getDrawable(R.drawable.ic_media_connecting_container).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                connecting = it
+            }
+    }
+
+    fun getPlayIcon(context: Context): AnimatedVectorDrawable? {
+        return playIcon?.let {
+            it.reset()
+            it
+        }
+            ?: (context.getDrawable(R.drawable.ic_media_play) as AnimatedVectorDrawable?).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                playIcon = it
+            }
+    }
+
+    fun getPlayBackground(context: Context): AnimatedVectorDrawable? {
+        return playBackground?.let {
+            it.reset()
+            it
+        }
+            ?: (context.getDrawable(R.drawable.ic_media_play_container) as AnimatedVectorDrawable?)
+                .also {
+                    if (!mediaControlsDrawablesReuse()) return@also
+                    playBackground = it
+                }
+    }
+
+    fun getPauseIcon(context: Context): AnimatedVectorDrawable? {
+        return pauseIcon?.let {
+            it.reset()
+            it
+        }
+            ?: (context.getDrawable(R.drawable.ic_media_pause) as AnimatedVectorDrawable?).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                pauseIcon = it
+            }
+    }
+
+    fun getPauseBackground(context: Context): AnimatedVectorDrawable? {
+        return pauseBackground?.let {
+            it.reset()
+            it
+        }
+            ?: (context.getDrawable(R.drawable.ic_media_pause_container) as AnimatedVectorDrawable?)
+                .also {
+                    if (!mediaControlsDrawablesReuse()) return@also
+                    pauseBackground = it
+                }
+    }
+
+    fun getNextIcon(context: Context): Drawable? {
+        return nextIcon
+            ?: context.getDrawable(R.drawable.ic_media_next).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                nextIcon = it
+            }
+    }
+
+    fun getPrevIcon(context: Context): Drawable? {
+        return prevIcon
+            ?: context.getDrawable(R.drawable.ic_media_prev).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                prevIcon = it
+            }
+    }
+
+    fun getLeAudioSharing(context: Context): Drawable? {
+        return leAudioSharing
+            ?: context.getDrawable(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                leAudioSharing = it
+            }
+    }
+
+    fun getAntenna(context: Context): Drawable? {
+        return antenna
+            ?: context.getDrawable(R.drawable.settings_input_antenna).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                antenna = it
+            }
+    }
+
+    fun getGroupDevice(context: Context): Drawable? {
+        return groupDevice
+            ?: context.getDrawable(com.android.settingslib.R.drawable.ic_media_group_device).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                groupDevice = it
+            }
+    }
+
+    fun getHomeDevices(context: Context): Drawable? {
+        return homeDevices
+            ?: context.getDrawable(R.drawable.ic_media_home_devices).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                homeDevices = it
+            }
+    }
+
+    fun getOutline(context: Context): Drawable? {
+        return outline
+            ?: context.getDrawable(R.drawable.qs_media_outline_button).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                outline = it
+            }
+    }
+
+    fun getSolid(context: Context): Drawable? {
+        return solid
+            ?: context.getDrawable(R.drawable.qs_media_solid_button).also {
+                if (!mediaControlsDrawablesReuse()) return@also
+                solid = it
+            }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
index 3c25e62..6373fed 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
@@ -163,6 +163,13 @@
         if (viewModel.playTurbulenceNoise) {
             viewController.setUpTurbulenceNoise()
         }
+
+        // TODO: We don't need to refresh this state constantly, only if the state actually changed
+        // to something which might impact the measurement
+        // State refresh interferes with the translation animation, only run it if it's not running.
+        if (!viewController.metadataAnimationHandler.isRunning) {
+            viewController.refreshState()
+        }
     }
 
     private fun bindOutputSwitcherModel(
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/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
index 16642ab..51c437dbe 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
@@ -52,9 +52,20 @@
         get() = prefs.getBoolean(HAS_APPROVED_SCREEN_RECORDING, false)
         private set(value) = prefs.edit().putBoolean(HAS_APPROVED_SCREEN_RECORDING, value).apply()
 
+    // Store the index of the issue type because res ids are generated at compile time and change
+    // in value from one build to another. The index will not change between package versions.
+    private var issueTypeIndex: Int
+        get() = prefs.getInt(KEY_ISSUE_TYPE_INDEX, ISSUE_TYPE_NOT_SET)
+        set(value) = prefs.edit().putInt(KEY_ISSUE_TYPE_INDEX, value).apply()
+
     var issueTypeRes
-        get() = prefs.getInt(KEY_ISSUE_TYPE_RES, ISSUE_TYPE_NOT_SET)
-        set(value) = prefs.edit().putInt(KEY_ISSUE_TYPE_RES, value).apply()
+        get() =
+            // If the user has never used the record issue tile, we don't show a default issue type
+            if (issueTypeIndex == ISSUE_TYPE_NOT_SET) ISSUE_TYPE_NOT_SET
+            else ALL_ISSUE_TYPES.keys.toIntArray()[issueTypeIndex]
+        set(value) {
+            issueTypeIndex = ALL_ISSUE_TYPES.keys.toIntArray().indexOf(value)
+        }
 
     val traceConfig: TraceConfig
         get() = ALL_ISSUE_TYPES[issueTypeRes] ?: customTraceState.traceConfig
@@ -89,17 +100,17 @@
         private const val HAS_APPROVED_SCREEN_RECORDING = "HasApprovedScreenRecord"
         private const val KEY_RECORD_SCREEN = "key_recordScreen"
         private const val KEY_TAG_TITLES = "key_tagTitles"
-        const val KEY_ISSUE_TYPE_RES = "key_issueTypeRes"
+        const val KEY_ISSUE_TYPE_INDEX = "key_issueTypeIndex"
         const val ISSUE_TYPE_NOT_SET = -1
         const val TAG_TITLE_DELIMITER = ": "
 
-        val ALL_ISSUE_TYPES: Map<Int, TraceConfig?> =
-            hashMapOf(
+        val ALL_ISSUE_TYPES: LinkedHashMap<Int, TraceConfig?> =
+            linkedMapOf(
                 Pair(R.string.performance, PresetTraceConfigs.getPerformanceConfig()),
                 Pair(R.string.user_interface, PresetTraceConfigs.getUiConfig()),
                 Pair(R.string.battery, PresetTraceConfigs.getBatteryConfig()),
                 Pair(R.string.thermal, PresetTraceConfigs.getThermalConfig()),
-                Pair(R.string.custom, null),
+                Pair(R.string.custom, null), // Null means we are using a custom trace config
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index f8b3ce1..ed67e64 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -42,7 +42,6 @@
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate
 import com.android.systemui.recordissue.IssueRecordingState.Companion.ALL_ISSUE_TYPES
 import com.android.systemui.recordissue.IssueRecordingState.Companion.ISSUE_TYPE_NOT_SET
-import com.android.systemui.recordissue.IssueRecordingState.Companion.KEY_ISSUE_TYPE_RES
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -51,6 +50,8 @@
 import dagger.assisted.AssistedInject
 import java.util.concurrent.Executor
 
+private const val EXTRA_ISSUE_TYPE_RES = "extra_issueTypeRes"
+
 class RecordIssueDialogDelegate
 @AssistedInject
 constructor(
@@ -170,7 +171,7 @@
             PopupMenu.OnMenuItemClickListener {
                 issueTypeButton.text = it.title
                 state.issueTypeRes =
-                    it.intent?.getIntExtra(KEY_ISSUE_TYPE_RES, ISSUE_TYPE_NOT_SET)
+                    it.intent?.getIntExtra(EXTRA_ISSUE_TYPE_RES, ISSUE_TYPE_NOT_SET)
                         ?: ISSUE_TYPE_NOT_SET
                 onIssueTypeSelected.run()
                 true
@@ -181,7 +182,7 @@
                 if (it != state.issueTypeRes) {
                     iconTintList = ColorStateList.valueOf(Color.TRANSPARENT)
                 }
-                intent = Intent().putExtra(KEY_ISSUE_TYPE_RES, it)
+                intent = Intent().putExtra(EXTRA_ISSUE_TYPE_RES, it)
 
                 if (it == R.string.custom) {
                     setOnMenuItemClickListener {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index e73664d..cc46216 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -152,7 +152,7 @@
             hydrateBackStack()
             resetShadeSessions()
             handleKeyguardEnabledness()
-            notifyKeyguardDismissCallbacks()
+            notifyKeyguardDismissCancelledCallbacks()
             refreshLockscreenEnabled()
         } else {
             sceneLogger.logFrameworkEnabled(
@@ -379,8 +379,10 @@
                     when {
                         isAlternateBouncerVisible -> {
                             // When the device becomes unlocked when the alternate bouncer is
-                            // showing, always hide the alternate bouncer...
+                            // showing, always hide the alternate bouncer and notify dismiss
+                            // succeeded
                             alternateBouncerInteractor.hide()
+                            dismissCallbackRegistry.notifyDismissSucceeded()
 
                             // ... and go to Gone or stay on the current scene
                             if (
@@ -394,9 +396,11 @@
                                 null
                             }
                         }
-                        isOnPrimaryBouncer ->
+                        isOnPrimaryBouncer -> {
                             // When the device becomes unlocked in primary Bouncer,
+                            // notify dismiss succeeded and
                             // go to previous scene or Gone.
+                            dismissCallbackRegistry.notifyDismissSucceeded()
                             if (
                                 previousScene.value == Scenes.Lockscreen ||
                                     !statusBarStateController.leaveOpenOnKeyguardHide()
@@ -410,6 +414,7 @@
                                     "device was unlocked with primary bouncer showing," +
                                         " from sceneKey=$prevScene"
                             }
+                        }
                         isOnLockscreen ->
                             // The lockscreen should be dismissed automatically in 2 scenarios:
                             // 1. When face auth bypass is enabled and authentication happens while
@@ -468,6 +473,9 @@
         applicationScope.launch {
             powerInteractor.isAsleep.collect { isAsleep ->
                 if (isAsleep) {
+                    alternateBouncerInteractor.hide()
+                    dismissCallbackRegistry.notifyDismissCancelled()
+
                     switchToScene(
                         targetSceneKey = Scenes.Lockscreen,
                         loggingReason = "device is starting to sleep",
@@ -771,15 +779,23 @@
         }
     }
 
-    private fun notifyKeyguardDismissCallbacks() {
+    private fun notifyKeyguardDismissCancelledCallbacks() {
         applicationScope.launch {
-            sceneInteractor.currentScene.pairwise().collect { (from, to) ->
-                when {
-                    from != Scenes.Bouncer -> Unit
-                    to == Scenes.Gone -> dismissCallbackRegistry.notifyDismissSucceeded()
-                    else -> dismissCallbackRegistry.notifyDismissCancelled()
+            combine(
+                    deviceEntryInteractor.isUnlocked,
+                    sceneInteractor.currentScene.pairwise(),
+                ) { isUnlocked, (from, to) ->
+                    when {
+                        from != Scenes.Bouncer -> false
+                        to != Scenes.Gone && !isUnlocked -> true
+                        else -> false
+                    }
                 }
-            }
+                .collect { notifyKeyguardDismissCancelled ->
+                    if (notifyKeyguardDismissCancelled) {
+                        dismissCallbackRegistry.notifyDismissCancelled()
+                    }
+                }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 65a59f5..c023b83 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -4536,6 +4536,13 @@
     private final class StatusBarStateListener implements StateListener {
         @Override
         public void onStateChanged(int statusBarState) {
+            onStateChanged(statusBarState, false);
+        }
+
+        private void onStateChanged(
+                int statusBarState,
+                boolean animatingUnlockedShadeToKeyguardBypass
+        ) {
             boolean goingToFullShade = mStatusBarStateController.goingToFullShade();
             boolean keyguardFadingAway = mKeyguardStateController.isKeyguardFadingAway();
             int oldState = mBarState;
@@ -4607,15 +4614,14 @@
                 //  - getting notified again about the current SHADE or KEYGUARD state
                 final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE
                         && statusBarState == KEYGUARD
-                        && mScreenOffAnimationController.isKeyguardShowDelayed();
+                        && mScreenOffAnimationController.isKeyguardShowDelayed()
+                        //Bypasses animatingUnlockedShadeToKeyguard for b/337742708
+                        && !animatingUnlockedShadeToKeyguardBypass;
                 if (!animatingUnlockedShadeToKeyguard) {
                     // Only make the status bar visible if we're not animating the screen off, since
                     // we only want to be showing the clock/notifications during the animation.
-                    if (keyguardShowing) {
-                        mShadeLog.v("Updating keyguard status bar state to visible");
-                    } else {
-                        mShadeLog.v("Updating keyguard status bar state to invisible");
-                    }
+                    mShadeLog.logKeyguardStatudBarVisibiliy(keyguardShowing, isOnAod(),
+                            animatingUnlockedShadeToKeyguardBypass, oldState, statusBarState);
                     mKeyguardStatusBarViewController.updateViewState(
                             /* alpha= */ 1f,
                             keyguardShowing ? View.VISIBLE : View.INVISIBLE);
@@ -4692,7 +4698,8 @@
                     .addTagListener(QS.TAG, mQsController.getQsFragmentListener());
             if (!SceneContainerFlag.isEnabled()) {
                 mStatusBarStateController.addCallback(mStatusBarStateListener);
-                mStatusBarStateListener.onStateChanged(mStatusBarStateController.getState());
+                // Bypass animatingUnlockedShadeToKeyguard in onStateChanged for b/337742708
+                mStatusBarStateListener.onStateChanged(mStatusBarStateController.getState(), true);
             }
             mConfigurationController.addCallback(mConfigurationListener);
             // Theme might have changed between inflating this view and attaching it to the
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 66a310c..f1eaec8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -411,4 +411,29 @@
             }
         )
     }
+
+    fun logKeyguardStatudBarVisibiliy(
+        visibility: Boolean,
+        isOnAod: Boolean,
+        animatingUnlockedShadeToKeyguardBypass: Boolean,
+        oldShadeState: Int,
+        newShadeState: Int,
+    ) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                bool1 = visibility
+                bool2 = isOnAod
+                bool3 = animatingUnlockedShadeToKeyguardBypass
+                int1 = oldShadeState
+                int2 = newShadeState
+            },
+            {
+                "Setting keyguard status bar visibility to: $bool1, isOnAod: $bool2" +
+                    "oldShadeState: $int1, newShadeState: $int2," +
+                    "animatingUnlockedShadeToKeyguardBypass: $bool3"
+            }
+        )
+    }
 }
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/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 94dd9bb..3dd265b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -91,6 +91,7 @@
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleRegistry;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.logging.MetricsLogger;
@@ -596,6 +597,8 @@
 
     private final EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
 
+    private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
+
     /**
      * Public constructor for CentralSurfaces.
      *
@@ -708,7 +711,8 @@
             ActivityStarter activityStarter,
             BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor,
             GlanceableHubContainerController glanceableHubContainerController,
-            EmergencyGestureIntentFactory emergencyGestureIntentFactory
+            EmergencyGestureIntentFactory emergencyGestureIntentFactory,
+            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager
     ) {
         mContext = context;
         mNotificationsController = notificationsController;
@@ -842,6 +846,8 @@
         mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy;
         mLightRevealScrim = lightRevealScrim;
 
+        mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager;
+
         if (PredictiveBackSysUiFlag.isEnabled()) {
             mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
         }
@@ -1683,7 +1689,7 @@
                         mNotificationShadeWindowController.setRequestTopUi(false, TAG);
                     }
                 }, /* isDozing= */ false, RippleShape.CIRCLE,
-                sUiEventLogger).show(animationDelay);
+                sUiEventLogger, mViewCaptureAwareWindowManager).show(animationDelay);
     }
 
     @Override
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/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index de4d14d..0f93ff2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -545,6 +545,7 @@
 
     @VisibleForTesting
     void consumeFromAlternateBouncerTransitionSteps(TransitionStep step) {
+        SceneContainerFlag.assertInLegacyMode();
         hideAlternateBouncer(false);
     }
 
@@ -554,6 +555,7 @@
      */
     @VisibleForTesting
     void consumeKeyguardAuthenticatedBiometricsHandled(Unit handled) {
+        SceneContainerFlag.assertInLegacyMode();
         if (mAlternateBouncerInteractor.isVisibleState()) {
             hideAlternateBouncer(false);
         }
@@ -981,7 +983,7 @@
             } else {
                 showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
             }
-            if (hideBouncerWhenShowing) {
+            if (!SceneContainerFlag.isEnabled() && hideBouncerWhenShowing) {
                 hideAlternateBouncer(true);
             }
             mKeyguardUpdateManager.sendKeyguardReset();
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/touchpad/tutorial/TouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt
index 238e8a1..3fa3f63 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt
@@ -20,6 +20,7 @@
 import androidx.compose.runtime.Composable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
 import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider
 import com.android.systemui.model.SysUiState
 import com.android.systemui.settings.DisplayTracker
@@ -53,9 +54,10 @@
         fun touchpadGesturesInteractor(
             sysUiState: SysUiState,
             displayTracker: DisplayTracker,
-            @Background backgroundScope: CoroutineScope
+            @Background backgroundScope: CoroutineScope,
+            logger: InputDeviceTutorialLogger,
         ): TouchpadGesturesInteractor {
-            return TouchpadGesturesInteractor(sysUiState, displayTracker, backgroundScope)
+            return TouchpadGesturesInteractor(sysUiState, displayTracker, backgroundScope, logger)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt
index df95232..1a41987 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.touchpad.tutorial.domain.interactor
 
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
 import com.android.systemui.model.SysUiState
 import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.shared.system.QuickStepContract
@@ -25,13 +26,16 @@
 class TouchpadGesturesInteractor(
     private val sysUiState: SysUiState,
     private val displayTracker: DisplayTracker,
-    private val backgroundScope: CoroutineScope
+    private val backgroundScope: CoroutineScope,
+    private val logger: InputDeviceTutorialLogger,
 ) {
     fun disableGestures() {
+        logger.log("Disabling touchpad gestures across the system")
         setGesturesState(disabled = true)
     }
 
     fun enableGestures() {
+        logger.log("Enabling touchpad gestures across the system")
         setGesturesState(disabled = false)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
index 256c5b5..821b51a 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
@@ -27,6 +27,8 @@
 import androidx.lifecycle.Lifecycle.State.STARTED
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.theme.PlatformTheme
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext
 import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen
 import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen
 import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen
@@ -42,6 +44,7 @@
 @Inject
 constructor(
     private val viewModelFactory: TouchpadTutorialViewModel.Factory,
+    private val logger: InputDeviceTutorialLogger,
 ) : ComponentActivity() {
 
     private val vm by viewModels<TouchpadTutorialViewModel>(factoryProducer = { viewModelFactory })
@@ -49,9 +52,17 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         enableEdgeToEdge()
-        setContent { PlatformTheme { TouchpadTutorialScreen(vm) { finish() } } }
+        setContent {
+            PlatformTheme { TouchpadTutorialScreen(vm, closeTutorial = ::finishTutorial) }
+        }
         // required to handle 3+ fingers on touchpad
         window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+        window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS)
+    }
+
+    private fun finishTutorial() {
+        logger.logCloseTutorial(TutorialContext.TOUCHPAD_TUTORIAL)
+        finish()
     }
 
     override fun onResume() {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
index d3aeaa7..43266ad 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
@@ -18,18 +18,23 @@
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext
 import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 
-class TouchpadTutorialViewModel(private val gesturesInteractor: TouchpadGesturesInteractor) :
-    ViewModel() {
+class TouchpadTutorialViewModel(
+    private val gesturesInteractor: TouchpadGesturesInteractor,
+    private val logger: InputDeviceTutorialLogger
+) : ViewModel() {
 
     private val _screen = MutableStateFlow(Screen.TUTORIAL_SELECTION)
     val screen: StateFlow<Screen> = _screen
 
     fun goTo(screen: Screen) {
+        logger.logGoingToScreen(screen, TutorialContext.TOUCHPAD_TUTORIAL)
         _screen.value = screen
     }
 
@@ -41,12 +46,16 @@
         gesturesInteractor.enableGestures()
     }
 
-    class Factory @Inject constructor(private val gesturesInteractor: TouchpadGesturesInteractor) :
-        ViewModelProvider.Factory {
+    class Factory
+    @Inject
+    constructor(
+        private val gesturesInteractor: TouchpadGesturesInteractor,
+        private val logger: InputDeviceTutorialLogger
+    ) : ViewModelProvider.Factory {
 
         @Suppress("UNCHECKED_CAST")
         override fun <T : ViewModel> create(modelClass: Class<T>): T {
-            return TouchpadTutorialViewModel(gesturesInteractor) as T
+            return TouchpadTutorialViewModel(gesturesInteractor, logger) as T
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
index ef9f8ff..ae0061b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
@@ -19,7 +19,7 @@
 import android.util.IndentingPrintWriter
 import com.android.systemui.Dumpable
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.lifecycle.SafeActivatable
+import com.android.systemui.lifecycle.BaseActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.util.asIndenting
 import com.android.systemui.util.printCollection
@@ -189,7 +189,7 @@
 ) : SimpleFlowDumper(), ActivatableFlowDumper {
 
     private val registration =
-        object : SafeActivatable() {
+        object : BaseActivatable() {
             override suspend fun onActivated(): Nothing {
                 try {
                     dumpManager.registerCriticalDumpable(
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/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index 44207a0..c6e4e0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -53,6 +53,7 @@
 
 import androidx.test.filters.LargeTest;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
@@ -112,6 +113,8 @@
     SysUiState mSysUiState;
     @Mock
     SecureSettings mSecureSettings;
+    @Mock
+    ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
     private SpyWindowMagnificationController mController;
     private WindowMagnificationController mSpyController;
     private WindowMagnificationAnimationController mWindowMagnificationAnimationController;
@@ -164,7 +167,8 @@
                 mSysUiState,
                 mSecureSettings,
                 scvhSupplier,
-                mSfVsyncFrameProvider);
+                mSfVsyncFrameProvider,
+                mViewCaptureAwareWindowManager);
 
         mSpyController = mController.getSpyController();
     }
@@ -1015,7 +1019,8 @@
                 SysUiState sysUiState,
                 SecureSettings secureSettings,
                 Supplier<SurfaceControlViewHost> scvhSupplier,
-                SfVsyncFrameCallbackProvider sfVsyncFrameProvider) {
+                SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
+                ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) {
             super(
                     context,
                     handler,
@@ -1027,7 +1032,8 @@
                     secureSettings,
                     scvhSupplier,
                     sfVsyncFrameProvider,
-                    WindowManagerGlobal::getWindowSession);
+                    WindowManagerGlobal::getWindowSession,
+                    viewCaptureAwareWindowManager);
             mSpyController = Mockito.mock(WindowMagnificationController.class);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index f57003e..25696bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -92,6 +92,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 
+import com.android.app.viewcapture.ViewCapture;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
@@ -107,6 +109,8 @@
 
 import com.google.common.util.concurrent.AtomicDouble;
 
+import kotlin.Lazy;
+
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
@@ -150,6 +154,8 @@
     private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
     @Mock
     private SecureSettings mSecureSettings;
+    @Mock
+    private Lazy<ViewCapture> mLazyViewCapture;
 
     private long mWaitAnimationDuration;
     private long mWaitBounceEffectDuration;
@@ -226,6 +232,9 @@
         when(mContext.getSharedPreferences(
                 eq("window_magnification_preferences"), anyInt()))
                 .thenReturn(mSharedPreferences);
+        ViewCaptureAwareWindowManager viewCaptureAwareWindowManager = new
+                ViewCaptureAwareWindowManager(mWindowManager, mLazyViewCapture,
+                /* isViewCaptureEnabled= */ false);
         mWindowMagnificationController =
                 new WindowMagnificationController(
                         mContext,
@@ -238,7 +247,8 @@
                         mSecureSettings,
                         /* scvhSupplier= */ () -> null,
                         mSfVsyncFrameProvider,
-                        /* globalWindowSessionSupplier= */ () -> mWindowSessionSpy);
+                        /* globalWindowSessionSupplier= */ () -> mWindowSessionSpy,
+                        viewCaptureAwareWindowManager);
 
         verify(mMirrorWindowControl).setWindowDelegate(
                 any(MirrorWindowControl.MirrorWindowDelegate.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
index 6ff1b81..9b09ec2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
@@ -90,6 +90,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
@@ -144,6 +145,8 @@
     private SurfaceControl.Transaction mTransaction;
     @Mock
     private SecureSettings mSecureSettings;
+    @Mock
+    private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
 
     private long mWaitAnimationDuration;
     private long mWaitBounceEffectDuration;
@@ -240,7 +243,8 @@
                         mSecureSettings,
                         scvhSupplier,
                         /* sfVsyncFrameProvider= */ null,
-                        /* globalWindowSessionSupplier= */ null);
+                        /* globalWindowSessionSupplier= */ null,
+                        mViewCaptureAwareWindowManager);
 
         verify(mMirrorWindowControl).setWindowDelegate(
                 any(MirrorWindowControl.MirrorWindowDelegate.class));
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/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index 32d059b..a0fe538b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.flags.EnableSceneContainer
@@ -29,6 +30,10 @@
 import com.android.systemui.keyguard.shared.model.DismissAction
 import com.android.systemui.keyguard.shared.model.KeyguardDone
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.scene.data.repository.Idle
 import com.android.systemui.scene.data.repository.Transition
 import com.android.systemui.scene.data.repository.setSceneTransition
@@ -82,6 +87,8 @@
                 deviceEntryInteractor = kosmos.deviceEntryInteractor,
                 quickSettingsSceneFamilyResolver = kosmos.quickSettingsSceneFamilyResolver,
                 notifShadeSceneFamilyResolver = kosmos.notifShadeSceneFamilyResolver,
+                powerInteractor = kosmos.powerInteractor,
+                alternateBouncerInteractor = kosmos.alternateBouncerInteractor,
             )
     }
 
@@ -234,6 +241,32 @@
         }
 
     @Test
+    fun resetDismissAction_onBouncer_OnAsleep() =
+        testScope.runTest {
+            kosmos.setSceneTransition(Idle(Scenes.Bouncer))
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.None
+            )
+            val resetDismissAction by collectLastValue(underTest.resetDismissAction)
+            keyguardRepository.setDismissAction(
+                DismissAction.RunAfterKeyguardGone(
+                    dismissAction = {},
+                    onCancelAction = {},
+                    message = "message",
+                    willAnimateOnLockscreen = true,
+                )
+            )
+            assertThat(resetDismissAction).isNull()
+            kosmos.fakePowerRepository.updateWakefulness(
+                rawState = WakefulnessState.ASLEEP,
+                lastWakeReason = WakeSleepReason.POWER_BUTTON,
+                lastSleepReason = WakeSleepReason.TIMEOUT,
+                powerButtonLaunchGestureTriggered = false,
+            )
+            assertThat(resetDismissAction).isEqualTo(Unit)
+        }
+
+    @Test
     fun setDismissAction_callsCancelRunnableOnPreviousDismissAction() =
         testScope.runTest {
             val dismissAction by collectLastValue(underTest.dismissAction)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index e9c16c2..c7c08a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -89,6 +89,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.compose.animation.scene.ObservableTransitionState;
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.logging.UiEventLogger;
@@ -344,6 +345,7 @@
     @Mock private GlanceableHubContainerController mGlanceableHubContainerController;
     @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
     @Mock private NotificationSettingsInteractor mNotificationSettingsInteractor;
+    @Mock private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
@@ -603,7 +605,8 @@
                 mActivityStarter,
                 mBrightnessMirrorShowingInteractor,
                 mGlanceableHubContainerController,
-                mEmergencyGestureIntentFactory
+                mEmergencyGestureIntentFactory,
+                mViewCaptureAwareWindowManager
         );
         mScreenLifecycle.addObserver(mCentralSurfaces.mScreenObserver);
         mCentralSurfaces.initShadeVisibilityListener();
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/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModelTest.kt
index c705cea..89e8895 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModelTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger
 import com.android.systemui.model.sysUiState
 import com.android.systemui.settings.displayTracker
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED
@@ -41,7 +42,13 @@
     private val sysUiState = kosmos.sysUiState
     private val viewModel =
         TouchpadTutorialViewModel(
-            TouchpadGesturesInteractor(sysUiState, kosmos.displayTracker, testScope.backgroundScope)
+            TouchpadGesturesInteractor(
+                sysUiState,
+                kosmos.displayTracker,
+                testScope.backgroundScope,
+                kosmos.inputDeviceTutorialLogger
+            ),
+            kosmos.inputDeviceTutorialLogger
         )
 
     @Test
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/data/GestureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialKosmos.kt
index 9bd346e..827f0d2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialKosmos.kt
@@ -14,12 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.gesture.data
+package com.android.systemui.inputdevice.tutorial
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
-import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl
+import org.mockito.kotlin.mock
 
-val Kosmos.gestureRepository: GestureRepository by
-    Kosmos.Fixture { GestureRepositoryImpl(testDispatcher) }
+var Kosmos.inputDeviceTutorialLogger: InputDeviceTutorialLogger by
+    Kosmos.Fixture { mock<InputDeviceTutorialLogger>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
index 957f092..27eadb1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.resolver.notifShadeSceneFamilyResolver
 import com.android.systemui.scene.domain.resolver.quickSettingsSceneFamilyResolver
@@ -37,5 +39,7 @@
             deviceEntryInteractor = deviceEntryInteractor,
             quickSettingsSceneFamilyResolver = quickSettingsSceneFamilyResolver,
             notifShadeSceneFamilyResolver = notifShadeSceneFamilyResolver,
+            powerInteractor = powerInteractor,
+            alternateBouncerInteractor = alternateBouncerInteractor,
         )
     }
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/FakeActivatable.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
index bcc7393..4c05939 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
@@ -21,7 +21,7 @@
 class FakeActivatable(
     private val onActivation: () -> Unit = {},
     private val onDeactivation: () -> Unit = {},
-) : SafeActivatable() {
+) : BaseActivatable() {
     var activationCount = 0
     var cancellationCount = 0
 
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/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialKosmos.kt
index f502df0..ee9a4d2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.touchpad.tutorial
 
+import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.sysUiState
@@ -24,5 +25,10 @@
 
 var Kosmos.touchpadGesturesInteractor: TouchpadGesturesInteractor by
     Kosmos.Fixture {
-        TouchpadGesturesInteractor(sysUiState, displayTracker, testScope.backgroundScope)
+        TouchpadGesturesInteractor(
+            sysUiState,
+            displayTracker,
+            testScope.backgroundScope,
+            inputDeviceTutorialLogger
+        )
     }
diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
similarity index 97%
rename from ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
rename to ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
index b477117..30abaa2 100644
--- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
+++ b/ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood;
+package com.android.ravenwoodtest;
 
 import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
diff --git a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java b/ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
similarity index 96%
rename from ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
rename to ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
index 1029ed2..e547114 100644
--- a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
+++ b/ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood.resapk_test;
+package com.android.ravenwoodtest.resapk_test;
 
 
 import static junit.framework.TestCase.assertTrue;
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
similarity index 99%
rename from ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
rename to ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
index 3332e24..633ed4e 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood.runtimetest;
+package com.android.ravenwoodtest.runtimetest;
 
 // Copied from libcore/luni/src/test/java/libcore/android/system/OsConstantsTest.java
 
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
similarity index 99%
rename from ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
rename to ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
index 05275b2..c2230c7 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood.runtimetest;
+package com.android.ravenwoodtest.runtimetest;
 
 import static android.system.OsConstants.S_ISBLK;
 import static android.system.OsConstants.S_ISCHR;
diff --git a/ravenwood/scripts/shrink-systemui-test b/ravenwood/scripts/shrink-systemui-test
new file mode 100755
index 0000000..8589c1d
--- /dev/null
+++ b/ravenwood/scripts/shrink-systemui-test
@@ -0,0 +1,131 @@
+#!/bin/bash
+# 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.
+
+set -e
+
+SCRIPT_NAME="${0##*/}"
+
+usage() {
+    cat <<"EOF"
+
+$SCRIPT_NAME: Shrink / unshrink SystemUiRavenTests.
+
+    SystemUiRavenTests has a lot of kotlin source files, so it's slow to build,
+    which is painful when you want to run it after updating ravenwood code
+    that SystemUiRavenTests depends on. (example: junit-src/)
+
+    This script basically removes the test files in SystemUI/multivalentTests
+    that don't have @EnabledOnRavenwood. But if we actaully remove them,
+    soong would re-generate the ninja file, which will take a long time,
+    so instead it'll truncate them.
+
+    This script will also tell git to ignore these files, so they won't shw up
+    in `git status`.
+    (Use `git ls-files -v | sed -ne "s/^[a-zS] //p"` to show ignored filse.)
+
+Usage:
+    $SCRIPT_NAME -s # Shrink the test files.
+
+    $SCRIPT_NAME -u # Undo it.
+
+EOF
+}
+
+TEST_PATH=${ANDROID_BUILD_TOP}/frameworks/base/packages/SystemUI/multivalentTests
+cd "$TEST_PATH"
+
+command=""
+case "$1" in
+    "-s") command=shrink ;;
+    "-u") command=unshrink ;;
+    *) usage ; exit 1 ;;
+esac
+
+
+echo "Listing test files...."
+files=( $(find . -name '*Test.kt' -o -name '*Test.java') )
+
+exemption='(BaseHeadsUpManagerTest)'
+
+shrink() {
+    local target=()
+    for file in ${files[@]}; do
+        # Check for exemption
+        if echo $file | egrep -q "$exemption"; then
+            echo "  Skip exempted file"
+            continue
+        fi
+
+        echo "Checking $file"
+        if ! [[ -f $file ]] ; then
+            echo "  Skip non regular file"
+            continue
+        fi
+
+        if ! [[ -s $file ]] ; then
+            echo "  Skip empty file"
+            continue
+        fi
+
+        if grep -q '@EnabledOnRavenwood' $file ; then
+            echo "  Skip ravenwood test file".
+            continue
+        fi
+
+        # It's a non ravenwood test file. Empty it.
+        : > $file
+
+        # Tell git to ignore the file
+
+        target+=($file)
+
+        echo "  Emptied"
+
+    done
+    if (( ${#target[@]} == 0 )) ; then
+        echo "No files emptied."
+        return 0
+    fi
+
+    git update-index --skip-worktree ${target[@]}
+
+    echo "Emptied ${#target[@]} files"
+    return 0
+}
+
+unshrink() {
+    local target=()
+
+    # Collect empty files
+    for file in ${files[@]}; do
+        if [[ -s $file ]] ; then
+            continue
+        fi
+
+        target+=($file)
+        : > $file
+    done
+    if (( ${#target[@]} == 0 )) ; then
+        echo "No files to restore."
+        return 0
+    fi
+    # Un-ignore the files, and check out the original files
+    echo "Restoring ${#target[@]} files..."
+    git update-index --no-skip-worktree ${target[@]}
+    git checkout goog/main ${target[@]}
+    return 0
+}
+
+$command
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index efa1397..3d7ad0b 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -35,7 +35,7 @@
         "androidx.annotation_annotation",
     ],
     static_libs: [
-        "a11ychecker-protos-java-proto-lite",
+        "accessibility_protos_lite",
         "com_android_server_accessibility_flags_lib",
         "//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib",
     ],
@@ -71,17 +71,6 @@
     aconfig_declarations: "com_android_server_accessibility_flags",
 }
 
-java_library_static {
-    name: "a11ychecker-protos-java-proto-lite",
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
-    srcs: [
-        "java/**/a11ychecker/proto/*.proto",
-    ],
-}
-
 genrule {
     name: "statslog-accessibility-java-gen",
     tools: ["stats-log-api-gen"],
diff --git a/services/accessibility/TEST_MAPPING b/services/accessibility/TEST_MAPPING
index 38b4148..454a329 100644
--- a/services/accessibility/TEST_MAPPING
+++ b/services/accessibility/TEST_MAPPING
@@ -25,15 +25,7 @@
       ]
     },
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.accessibility"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
+      "name": "FrameworksServicesTests_accessibility_Presubmit"
     },
     {
       "name": "FrameworksCoreTests_accessibility_NO_FLAKES"
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java
index f7a59a4b..83f57b2 100644
--- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java
@@ -30,7 +30,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.accessibility.Flags;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
 
 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
@@ -58,7 +57,7 @@
     private final PackageManager mPackageManager;
     private final Set<AccessibilityHierarchyCheck> mHierarchyChecks;
     private final ATFHierarchyBuilder mATFHierarchyBuilder;
-    private final Set<AccessibilityCheckResultReported> mCachedResults = new HashSet<>();
+    private final Set<AndroidAccessibilityCheckerResult> mCachedResults = new HashSet<>();
 
     @VisibleForTesting
     final A11yCheckerTimer mTimer = new A11yCheckerTimer();
@@ -85,14 +84,14 @@
      * logging. Returns the check results for the given nodes.
      */
     @RequiresPermission(allOf = {android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
-    public Set<AccessibilityCheckResultReported> maybeRunA11yChecker(
+    public Set<AndroidAccessibilityCheckerResult> maybeRunA11yChecker(
             List<AccessibilityNodeInfo> nodes, @Nullable String sourceEventClassName,
             ComponentName a11yServiceComponentName, @UserIdInt int userId) {
         if (!shouldRunA11yChecker()) {
             return Set.of();
         }
 
-        Set<AccessibilityCheckResultReported> allResults = new HashSet<>();
+        Set<AndroidAccessibilityCheckerResult> allResults = new HashSet<>();
         String defaultBrowserName = mPackageManager.getDefaultBrowserPackageNameAsUser(userId);
 
         try {
@@ -104,7 +103,7 @@
                     continue;
                 }
                 List<AccessibilityHierarchyCheckResult> checkResults = runChecksOnNode(nodeInfo);
-                Set<AccessibilityCheckResultReported> filteredResults =
+                Set<AndroidAccessibilityCheckerResult> filteredResults =
                         AccessibilityCheckerUtils.processResults(nodeInfo, checkResults,
                                 sourceEventClassName, mPackageManager, a11yServiceComponentName);
                 allResults.addAll(filteredResults);
@@ -127,7 +126,7 @@
         return checkResults;
     }
 
-    public Set<AccessibilityCheckResultReported> getCachedResults() {
+    public Set<AndroidAccessibilityCheckerResult> getCachedResults() {
         return Collections.unmodifiableSet(mCachedResults);
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java
index 1b3ec5a..fa0bb59 100644
--- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java
@@ -16,10 +16,9 @@
 
 package com.android.server.accessibility.a11ychecker;
 
+import android.text.TextUtils;
 import android.util.Slog;
 
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
-
 import java.util.Set;
 
 
@@ -35,11 +34,11 @@
     /**
      * Writes results to statsd.
      */
-    public static void logResults(Set<AccessibilityCheckResultReported> results) {
-        Slog.i(LOG_TAG, String.format("Writing %d AccessibilityCheckResultReported events",
+    public static void logResults(Set<AndroidAccessibilityCheckerResult> results) {
+        Slog.i(LOG_TAG, TextUtils.formatSimple("Writing %d AccessibilityCheckResultReported events",
                 results.size()));
 
-        for (AccessibilityCheckResultReported result : results) {
+        for (AndroidAccessibilityCheckerResult result : results) {
             AccessibilityCheckerStatsLog.write(ATOM_ID,
                     result.getPackageName(),
                     result.getAppVersionCode(),
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
index fa0fed2..eb24b027 100644
--- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
@@ -17,6 +17,8 @@
 package com.android.server.accessibility.a11ychecker;
 
 
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.pm.PackageInfo;
@@ -25,9 +27,6 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckClass;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
-import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultType;
 
 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
@@ -92,7 +91,7 @@
                             AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK));
     // LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto)
 
-    static Set<AccessibilityCheckResultReported> processResults(
+    static Set<AndroidAccessibilityCheckerResult> processResults(
             AccessibilityNodeInfo nodeInfo,
             List<AccessibilityHierarchyCheckResult> checkResults,
             @Nullable String activityClassName,
@@ -103,16 +102,16 @@
         if (nodePath == null) {
             return Set.of();
         }
-        AccessibilityCheckResultReported.Builder builder;
+        AndroidAccessibilityCheckerResult.Builder commonBuilder;
         try {
-            builder = AccessibilityCheckResultReported.newBuilder()
+            commonBuilder = AndroidAccessibilityCheckerResult.newBuilder()
                     .setPackageName(appPackageName)
                     .setAppVersionCode(getAppVersionCode(packageManager, appPackageName))
                     .setUiElementPath(nodePath)
                     .setActivityName(
                             getActivityName(packageManager, appPackageName, activityClassName))
                     .setWindowTitle(getWindowTitle(nodeInfo))
-                    .setSourceComponentName(a11yServiceComponentName.flattenToString())
+                    .setSourceComponentName(a11yServiceComponentName)
                     .setSourceVersionCode(
                             getAppVersionCode(packageManager,
                                     a11yServiceComponentName.getPackageName()));
@@ -126,7 +125,8 @@
                         == AccessibilityCheckResult.AccessibilityCheckResultType.ERROR
                         || checkResult.getType()
                         == AccessibilityCheckResult.AccessibilityCheckResultType.WARNING)
-                .map(checkResult -> builder.setResultCheckClass(
+                .map(checkResult -> new AndroidAccessibilityCheckerResult.Builder(
+                        commonBuilder).setResultCheckClass(
                         getCheckClass(checkResult)).setResultType(
                         getCheckResultType(checkResult)).setResultId(
                         checkResult.getResultId()).build())
@@ -188,9 +188,9 @@
     private static AccessibilityCheckResultType getCheckResultType(
             AccessibilityHierarchyCheckResult checkResult) {
         return switch (checkResult.getType()) {
-            case ERROR -> AccessibilityCheckResultType.ERROR;
-            case WARNING -> AccessibilityCheckResultType.WARNING;
-            default -> AccessibilityCheckResultType.UNKNOWN_RESULT_TYPE;
+            case ERROR -> AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE;
+            case WARNING -> AccessibilityCheckResultType.WARNING_CHECK_RESULT_TYPE;
+            default -> AccessibilityCheckResultType.UNKNOWN_CHECK_RESULT_TYPE;
         };
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AndroidAccessibilityCheckerResult.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AndroidAccessibilityCheckerResult.java
new file mode 100644
index 0000000..c9cd9fe
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AndroidAccessibilityCheckerResult.java
@@ -0,0 +1,196 @@
+/*
+ * 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.accessibility.a11ychecker;
+
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
+import android.content.ComponentName;
+import android.text.TextUtils;
+
+public class AndroidAccessibilityCheckerResult implements Cloneable {
+    // Package name of the app containing the checked View.
+    private String mPackageName;
+    // Version code of the app containing the checked View.
+    private long mAppVersionCode;
+    // The path of the View starting from the root element in the window. Each element is
+    // represented by the View's resource id, when available, or the View's class name.
+    private String mUiElementPath;
+    // Class name of the activity containing the checked View.
+    private String mActivityName;
+    // Title of the window containing the checked View.
+    private String mWindowTitle;
+    // The component name of the app running the AccessibilityService which provided the a11y node.
+    private String mSourceComponentName;
+    // Version code of the app running the AccessibilityService that provided the a11y node.
+    private long mSourceVersionCode;
+    // Class Name of the AccessibilityCheck that produced the result.
+    private AccessibilityCheckClass mResultCheckClass;
+    // Result type of the AccessibilityCheckResult.
+    private AccessibilityCheckResultType mResultType;
+    // Result ID of the AccessibilityCheckResult.
+    private int mResultId;
+
+    static final class Builder {
+        private final AndroidAccessibilityCheckerResult mInstance;
+
+        Builder() {
+            mInstance = new AndroidAccessibilityCheckerResult();
+        }
+
+        Builder(Builder otherBuilder) {
+            mInstance = otherBuilder.mInstance.clone();
+        }
+
+        public Builder setPackageName(String packageName) {
+            mInstance.mPackageName = packageName;
+            return this;
+        }
+
+        public Builder setAppVersionCode(long versionCode) {
+            mInstance.mAppVersionCode = versionCode;
+            return this;
+        }
+
+        public Builder setUiElementPath(String uiElementPath) {
+            mInstance.mUiElementPath = uiElementPath;
+            return this;
+        }
+
+        public Builder setActivityName(String activityName) {
+            mInstance.mActivityName = activityName;
+            return this;
+        }
+
+        public Builder setWindowTitle(String windowTitle) {
+            mInstance.mWindowTitle = windowTitle;
+            return this;
+        }
+
+        public Builder setSourceComponentName(ComponentName componentName) {
+            mInstance.mSourceComponentName = componentName.flattenToString();
+            return this;
+        }
+
+        public Builder setSourceVersionCode(long versionCode) {
+            mInstance.mSourceVersionCode = versionCode;
+            return this;
+        }
+
+        public Builder setResultCheckClass(AccessibilityCheckClass checkClass) {
+            mInstance.mResultCheckClass = checkClass;
+            return this;
+        }
+
+        public Builder setResultType(AccessibilityCheckResultType resultType) {
+            mInstance.mResultType = resultType;
+            return this;
+        }
+
+        public Builder setResultId(int resultId) {
+            mInstance.mResultId = resultId;
+            return this;
+        }
+
+        public AndroidAccessibilityCheckerResult build() {
+            // TODO: assert all fields are set, etc
+            return mInstance;
+        }
+    }
+
+    static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    public long getAppVersionCode() {
+        return mAppVersionCode;
+    }
+
+    public String getUiElementPath() {
+        return mUiElementPath;
+    }
+
+    public String getActivityName() {
+        return mActivityName;
+    }
+
+    public String getWindowTitle() {
+        return mWindowTitle;
+    }
+
+    public String getSourceComponentName() {
+        return mSourceComponentName;
+    }
+
+    public long getSourceVersionCode() {
+        return mSourceVersionCode;
+    }
+
+    public AccessibilityCheckClass getResultCheckClass() {
+        return mResultCheckClass;
+    }
+
+    public AccessibilityCheckResultType getResultType() {
+        return mResultType;
+    }
+
+    public int getResultId() {
+        return mResultId;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof AndroidAccessibilityCheckerResult)) {
+            return false;
+        }
+        AndroidAccessibilityCheckerResult otherResult = (AndroidAccessibilityCheckerResult) other;
+        return mPackageName.equals(otherResult.mPackageName)
+                && mAppVersionCode == otherResult.mAppVersionCode
+                && mUiElementPath.equals(otherResult.mUiElementPath)
+                && mActivityName.equals(otherResult.mActivityName)
+                && mWindowTitle.equals(otherResult.mWindowTitle)
+                && mSourceComponentName.equals(otherResult.mSourceComponentName)
+                && mSourceVersionCode == otherResult.mSourceVersionCode
+                && mResultCheckClass.equals(otherResult.mResultCheckClass)
+                && mResultType.equals(otherResult.mResultType)
+                && mResultId == otherResult.mResultId;
+    }
+
+    @Override
+    public int hashCode() {
+        return toString().hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return TextUtils.formatSimple("%s:%d:%s:%s:%s:%s:%d:%s:%s:%d", mPackageName,
+                mAppVersionCode, mUiElementPath, mActivityName, mWindowTitle, mSourceComponentName,
+                mSourceVersionCode, mResultCheckClass.name(), mResultType.name(), mResultId);
+    }
+
+    @Override
+    public AndroidAccessibilityCheckerResult clone() {
+        try {
+            return (AndroidAccessibilityCheckerResult) super.clone();
+        } catch (CloneNotSupportedException e) {
+            return null;
+        }
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
deleted file mode 100644
index 8beed4a..0000000
--- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-syntax = "proto2";
-package android.accessibility;
-
-option java_package = "com.android.server.accessibility.a11ychecker";
-option java_outer_classname = "A11yCheckerProto";
-
-// TODO(b/326385939): remove and replace usage with the atom extension proto, when submitted.
-/** Logs the result of an AccessibilityCheck. */
-message AccessibilityCheckResultReported {
-  // Package name of the app containing the checked View.
-  optional string package_name = 1;
-  // Version code of the app containing the checked View.
-  optional int64 app_version_code = 2;
-  // The path of the View starting from the root element in the window. Each element is
-  // represented by the View's resource id, when available, or the View's class name.
-  optional string ui_element_path = 3;
-  // Class name of the activity containing the checked View.
-  optional string activity_name = 4;
-  // Title of the window containing the checked View.
-  optional string window_title = 5;
-  // The flattened component name of the app running the AccessibilityService which provided the a11y node.
-  optional string source_component_name = 6;
-  // Version code of the app running the AccessibilityService that provided the a11y node.
-  optional int64 source_version_code = 7;
-  // Class Name of the AccessibilityCheck that produced the result.
-  optional AccessibilityCheckClass result_check_class = 8;
-  // Result type of the AccessibilityCheckResult.
-  optional AccessibilityCheckResultType result_type = 9;
-  // Result ID of the AccessibilityCheckResult.
-  optional int32 result_id = 10;
-}
-
-/** The AccessibilityCheck class. */
-// LINT.IfChange
-enum AccessibilityCheckClass {
-  UNKNOWN_CHECK = 0;
-  CLASS_NAME_CHECK = 1;
-  CLICKABLE_SPAN_CHECK = 2;
-  DUPLICATE_CLICKABLE_BOUNDS_CHECK = 3;
-  DUPLICATE_SPEAKABLE_TEXT_CHECK = 4;
-  EDITABLE_CONTENT_DESC_CHECK = 5;
-  IMAGE_CONTRAST_CHECK = 6;
-  LINK_PURPOSE_UNCLEAR_CHECK = 7;
-  REDUNDANT_DESCRIPTION_CHECK = 8;
-  SPEAKABLE_TEXT_PRESENT_CHECK = 9;
-  TEXT_CONTRAST_CHECK = 10;
-  TEXT_SIZE_CHECK = 11;
-  TOUCH_TARGET_SIZE_CHECK = 12;
-  TRAVERSAL_ORDER_CHECK = 13;
-}
-// LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java)
-
-/** The type of AccessibilityCheckResult */
-enum AccessibilityCheckResultType {
-  UNKNOWN_RESULT_TYPE = 0;
-  ERROR = 1;
-  WARNING = 2;
-}
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/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index 45d7206..7faeb83 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -50,18 +50,7 @@
       ]
     },
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.am."
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.Presubmit"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_am_Presubmit"
     },
     {
       "name": "FrameworksMockingServicesTests",
@@ -83,10 +72,7 @@
     },
     {
       "file_patterns": ["Battery[^/]*\\.java", "MeasuredEnergy[^/]*\\.java"],
-      "name": "FrameworksServicesTests",
-      "options": [
-        { "include-filter": "com.android.server.am.BatteryStatsServiceTest" }
-      ]
+      "name": "FrameworksServicesTests_battery_stats"
     },
     {
       "file_patterns": ["Battery[^/]*\\.java", "MeasuredEnergy[^/]*\\.java"],
diff --git a/services/core/java/com/android/server/appop/TEST_MAPPING b/services/core/java/com/android/server/appop/TEST_MAPPING
index 65f6af7..e7623e0 100644
--- a/services/core/java/com/android/server/appop/TEST_MAPPING
+++ b/services/core/java/com/android/server/appop/TEST_MAPPING
@@ -12,12 +12,7 @@
             "name": "CtsAppOps2TestCases"
         },
         {
-            "name": "FrameworksServicesTests",
-            "options": [
-                {
-                    "include-filter": "com.android.server.appop"
-                }
-            ]
+            "name": "FrameworksServicesTests_android_server_appop"
         },
         {
             "name": "FrameworksMockingServicesTests",
diff --git a/services/core/java/com/android/server/audio/TEST_MAPPING b/services/core/java/com/android/server/audio/TEST_MAPPING
index f050090..368b828 100644
--- a/services/core/java/com/android/server/audio/TEST_MAPPING
+++ b/services/core/java/com/android/server/audio/TEST_MAPPING
@@ -29,21 +29,7 @@
             ]
         },
         {
-            "name": "FrameworksServicesTests",
-            "options": [
-                {
-                  "include-filter": "com.android.server.audio"
-                },
-                {
-                  "include-annotation": "android.platform.test.annotations.Presubmit"
-                },
-                {
-                  "exclude-annotation": "androidx.test.filters.FlakyTest"
-                },
-                {
-                  "exclude-annotation": "org.junit.Ignore"
-                }
-            ]
+            "name": "FrameworksServicesTests_android_server_audio"
         }
     ]
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
index 59e64cd..87bd807 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java
@@ -121,6 +121,11 @@
             final int targetId = getTargetUserId();
             Slog.d(TAG, "Setting active user: " + targetId);
             HidlToAidlSessionAdapter sessionAdapter = (HidlToAidlSessionAdapter) getFreshDaemon();
+            if (sessionAdapter.getIBiometricsFingerprint() == null) {
+                Slog.e(TAG, "Failed to setActiveGroup: HIDL daemon is null.");
+                mCallback.onClientFinished(this, false /* success */);
+                return;
+            }
             sessionAdapter.setActiveGroup(targetId, mDirectory.getAbsolutePath());
             mAuthenticatorIds.put(targetId, mHasEnrolledBiometrics
                     ? sessionAdapter.getAuthenticatorIdForUpdateClient() : 0L);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
index b469752..671bd87 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java
@@ -209,6 +209,10 @@
         return null;
     }
 
+    protected IBiometricsFingerprint getIBiometricsFingerprint() {
+        return mSession.get();
+    }
+
     public long getAuthenticatorIdForUpdateClient() throws RemoteException {
         return mSession.get().getAuthenticatorId();
     }
diff --git a/services/core/java/com/android/server/compat/TEST_MAPPING b/services/core/java/com/android/server/compat/TEST_MAPPING
index bc1c728..3997bcf 100644
--- a/services/core/java/com/android/server/compat/TEST_MAPPING
+++ b/services/core/java/com/android/server/compat/TEST_MAPPING
@@ -2,12 +2,7 @@
     "presubmit": [
         // Unit tests
         {
-            "name": "FrameworksServicesTests",
-            "options": [
-                {
-                    "include-filter": "com.android.server.compat"
-                }
-            ]
+            "name": "FrameworksServicesTests_android_server_compat"
         },
         // Tests for the TestRule
         {
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/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 154710f..81c30dd 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1225,11 +1225,15 @@
         boolean avrSystemAudioMode = HdmiUtils.parseCommandParamSystemAudioStatus(message);
         // Set System Audio Mode according to TV's settings.
         // Handle <System Audio Mode Status> here only when
-        // SystemAudioAutoInitiationAction timeout
+        // SystemAudioAutoInitiationAction timeout.
+        // If AVR reports SAM on and it is in standby, the action SystemAudioActionFromTv
+        // triggers a <SAM Request> that will wake-up the AVR.
         HdmiDeviceInfo avr = getAvrDeviceInfo();
         if (avr == null) {
             setSystemAudioMode(false);
-        } else if (avrSystemAudioMode != tvSystemAudioMode) {
+        } else if (avrSystemAudioMode != tvSystemAudioMode
+                    || (avrSystemAudioMode && avr.getDevicePowerStatus()
+                        == HdmiControlManager.POWER_STATUS_STANDBY)) {
             addAndStartAction(new SystemAudioActionFromTv(this, avr.getLogicalAddress(),
                     tvSystemAudioMode, null));
         } else {
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
index 56e538b..028637b 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
@@ -16,7 +16,9 @@
 
 package com.android.server.hdmi;
 
+import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
@@ -89,8 +91,13 @@
 
         // If System Audio Control feature is enabled, turn on system audio mode when new AVR is
         // detected. Otherwise, turn off system audio mode.
+        // If AVR reports SAM on and it is in standby, the action SystemAudioActionFromTv
+        // triggers a <SAM Request> that will wake-up the AVR.
         boolean targetSystemAudioMode = tv().isSystemAudioControlFeatureEnabled();
-        if (currentSystemAudioMode != targetSystemAudioMode) {
+        if (currentSystemAudioMode != targetSystemAudioMode
+                || (currentSystemAudioMode && tv().getAvrDeviceInfo() != null
+                && tv().getAvrDeviceInfo().getDevicePowerStatus()
+                == HdmiControlManager.POWER_STATUS_STANDBY)) {
             // Start System Audio Control feature actions only if necessary.
             addAndStartAction(
                     new SystemAudioActionFromTv(tv(), mAvrAddress, targetSystemAudioMode, null));
diff --git a/services/core/java/com/android/server/hdmi/TEST_MAPPING b/services/core/java/com/android/server/hdmi/TEST_MAPPING
index c0fa121..1c85c7f 100644
--- a/services/core/java/com/android/server/hdmi/TEST_MAPPING
+++ b/services/core/java/com/android/server/hdmi/TEST_MAPPING
@@ -1,21 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.hdmi"
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.Presubmit"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_hdmi_Presubmit"
     }
   ],
   "postsubmit": [
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/integrity/TEST_MAPPING b/services/core/java/com/android/server/integrity/TEST_MAPPING
index be8d2e1..5c05fce 100644
--- a/services/core/java/com/android/server/integrity/TEST_MAPPING
+++ b/services/core/java/com/android/server/integrity/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.integrity."
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_integrity"
     },
     {
       "name": "GtsSecurityHostTestCases",
diff --git a/services/core/java/com/android/server/lights/TEST_MAPPING b/services/core/java/com/android/server/lights/TEST_MAPPING
index 17b98ce8..1d2cd3c 100644
--- a/services/core/java/com/android/server/lights/TEST_MAPPING
+++ b/services/core/java/com/android/server/lights/TEST_MAPPING
@@ -9,11 +9,7 @@
       ]
     },
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {"include-filter": "com.android.server.lights"},
-        {"exclude-annotation": "androidx.test.filters.FlakyTest"}
-      ]
+      "name": "FrameworksServicesTests_android_server_lights"
     }
   ]
 }
diff --git a/services/core/java/com/android/server/locales/TEST_MAPPING b/services/core/java/com/android/server/locales/TEST_MAPPING
index fd8cddc..26e4685 100644
--- a/services/core/java/com/android/server/locales/TEST_MAPPING
+++ b/services/core/java/com/android/server/locales/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.locales."
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_locales"
     },
     {
       "name": "CtsLocaleManagerHostTestCases"
diff --git a/services/core/java/com/android/server/location/contexthub/TEST_MAPPING b/services/core/java/com/android/server/location/contexthub/TEST_MAPPING
index 2f6aa53..85ea5a4 100644
--- a/services/core/java/com/android/server/location/contexthub/TEST_MAPPING
+++ b/services/core/java/com/android/server/location/contexthub/TEST_MAPPING
@@ -1,21 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.location.contexthub."
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.Presubmit"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_location_contexthub_Presubmit"
     }
   ],
   "imports": [
diff --git a/services/core/java/com/android/server/locksettings/TEST_MAPPING b/services/core/java/com/android/server/locksettings/TEST_MAPPING
index 256d9ba..ffbdf7f 100644
--- a/services/core/java/com/android/server/locksettings/TEST_MAPPING
+++ b/services/core/java/com/android/server/locksettings/TEST_MAPPING
@@ -14,15 +14,7 @@
     ],
     "presubmit": [
         {
-            "name": "FrameworksServicesTests",
-            "options": [
-                {
-                    "include-filter": "com.android.server.locksettings."
-                },
-                {
-                    "exclude-annotation": "androidx.test.filters.FlakyTest"
-                }
-            ]
+            "name": "FrameworksServicesTests_android_server_locksettings"
         }
     ],
     "postsubmit": [
diff --git a/services/core/java/com/android/server/logcat/TEST_MAPPING b/services/core/java/com/android/server/logcat/TEST_MAPPING
index 9041552..5b07cd9 100644
--- a/services/core/java/com/android/server/logcat/TEST_MAPPING
+++ b/services/core/java/com/android/server/logcat/TEST_MAPPING
@@ -1,11 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {"include-filter": "com.android.server.logcat"},
-        {"exclude-annotation": "androidx.test.filters.FlakyTest"}
-      ]
+      "name": "FrameworksServicesTests_android_server_logcat_Presubmit"
     }
   ],
   "postsubmit": [
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/om/TEST_MAPPING b/services/core/java/com/android/server/om/TEST_MAPPING
index 82e7817..ce047bb 100644
--- a/services/core/java/com/android/server/om/TEST_MAPPING
+++ b/services/core/java/com/android/server/om/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.om."
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_om"
     },
     {
       "name": "OverlayDeviceTests"
diff --git a/services/core/java/com/android/server/pdb/TEST_MAPPING b/services/core/java/com/android/server/pdb/TEST_MAPPING
index 9e98023..ed6dfd8 100644
--- a/services/core/java/com/android/server/pdb/TEST_MAPPING
+++ b/services/core/java/com/android/server/pdb/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
     "presubmit": [
         {
-            "name": "FrameworksServicesTests",
-            "options": [
-                {
-                    "include-filter": "com.android.server.pdb.PersistentDataBlockServiceTest"
-                }
-            ]
+            "name": "FrameworksServicesTests_android_server_pdb"
         }
     ]
 }
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index 2c13bd0..18d2390a 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -104,6 +104,54 @@
           "include-filter": "android.appsecurity.cts.EphemeralTest#testGetSearchableInfo"
         }
       ]
+    },
+    {
+      "name": "CtsPackageInstallerCUJInstallationTestCases",
+      "file_patterns": [
+        "core/java/.*Install.*",
+        "services/core/.*Install.*",
+        "services/core/java/com/android/server/pm/.*"
+      ],
+      "options":[
+          {
+              "exclude-annotation":"androidx.test.filters.FlakyTest"
+          },
+          {
+              "exclude-annotation":"org.junit.Ignore"
+          }
+      ]
+    },
+    {
+      "name": "CtsPackageInstallerCUJUninstallationTestCases",
+      "file_patterns": [
+        "core/java/.*Install.*",
+        "services/core/.*Install.*",
+        "services/core/java/com/android/server/pm/.*"
+      ],
+      "options":[
+          {
+              "exclude-annotation":"androidx.test.filters.FlakyTest"
+          },
+          {
+              "exclude-annotation":"org.junit.Ignore"
+          }
+      ]
+    },
+    {
+      "name": "CtsPackageInstallerCUJUpdateSelfTestCases",
+      "file_patterns": [
+        "core/java/.*Install.*",
+        "services/core/.*Install.*",
+        "services/core/java/com/android/server/pm/.*"
+      ],
+      "options":[
+          {
+              "exclude-annotation":"androidx.test.filters.FlakyTest"
+          },
+          {
+              "exclude-annotation":"org.junit.Ignore"
+          }
+      ]
     }
   ],
   "presubmit-large":[
diff --git a/services/core/java/com/android/server/pm/dex/TEST_MAPPING b/services/core/java/com/android/server/pm/dex/TEST_MAPPING
index 1c86c4f..64bcc22 100644
--- a/services/core/java/com/android/server/pm/dex/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/dex/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.pm.dex"
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_pm_dex"
     },
     {
       "name": "DynamicCodeLoggerIntegrationTests"
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/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
index 1ca267e99..89fa9b6 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsService.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -304,12 +304,22 @@
     @Override
     public void onStart() {
         if (getPowerStatsHal().isInitialized()) {
-            mPowerStatsInternal = new LocalService();
-            publishLocalService(PowerStatsInternal.class, mPowerStatsInternal);
+            publishLocalService(PowerStatsInternal.class, getPowerStatsInternal());
         }
         publishBinderService(Context.POWER_STATS_SERVICE, mService);
     }
 
+    /**
+     * Returns the PowerStatsInternal associated with this service, maybe creating it if needed.
+     */
+    @VisibleForTesting
+    public PowerStatsInternal getPowerStatsInternal() {
+        if (mPowerStatsInternal == null) {
+            mPowerStatsInternal = new LocalService();
+        }
+        return mPowerStatsInternal;
+    }
+
     private void onSystemServicesReady() {
         mPullAtomCallback = mInjector.createStatsPullerImpl(mContext, mPowerStatsInternal);
         mDeviceConfigListener.startListening();
@@ -456,7 +466,13 @@
 
     private void getEnergyConsumedAsync(CompletableFuture<EnergyConsumerResult[]> future,
             int[] energyConsumerIds) {
-        EnergyConsumerResult[] results = getPowerStatsHal().getEnergyConsumed(energyConsumerIds);
+        EnergyConsumerResult[] results;
+        try {
+            results = getPowerStatsHal().getEnergyConsumed(energyConsumerIds);
+        } catch (Exception e) {
+            future.completeExceptionally(e);
+            return;
+        }
 
         // STOPSHIP(253292374): Remove once missing EnergyConsumer results issue is resolved.
         EnergyConsumer[] energyConsumers = getEnergyConsumerInfo();
@@ -523,12 +539,20 @@
 
     private void getStateResidencyAsync(CompletableFuture<StateResidencyResult[]> future,
             int[] powerEntityIds) {
-        future.complete(getPowerStatsHal().getStateResidency(powerEntityIds));
+        try {
+            future.complete(getPowerStatsHal().getStateResidency(powerEntityIds));
+        } catch (Exception e) {
+            future.completeExceptionally(e);
+        }
     }
 
     private void readEnergyMeterAsync(CompletableFuture<EnergyMeasurement[]> future,
             int[] channelIds) {
-        future.complete(getPowerStatsHal().readEnergyMeter(channelIds));
+        try {
+            future.complete(getPowerStatsHal().readEnergyMeter(channelIds));
+        } catch (Exception e) {
+            future.completeExceptionally(e);
+        }
     }
 
     private static class PowerMonitorState {
diff --git a/services/core/java/com/android/server/powerstats/TEST_MAPPING b/services/core/java/com/android/server/powerstats/TEST_MAPPING
index 79224a5..0ba1da9a95 100644
--- a/services/core/java/com/android/server/powerstats/TEST_MAPPING
+++ b/services/core/java/com/android/server/powerstats/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.powerstats"
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_powerstats"
     }
   ]
 }
diff --git a/services/core/java/com/android/server/rollback/TEST_MAPPING b/services/core/java/com/android/server/rollback/TEST_MAPPING
index 2cc931b..291b8db 100644
--- a/services/core/java/com/android/server/rollback/TEST_MAPPING
+++ b/services/core/java/com/android/server/rollback/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.rollback"
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_rollback"
     }
   ],
   "imports": [
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 331a594..ac56043 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -70,6 +70,7 @@
 import static com.android.server.stats.pull.ProcfsMemoryUtil.getProcessCmdlines;
 import static com.android.server.stats.pull.ProcfsMemoryUtil.readCmdlineFromProcfs;
 import static com.android.server.stats.pull.ProcfsMemoryUtil.readMemorySnapshotFromProcfs;
+import static com.android.server.stats.pull.netstats.NetworkStatsUtils.fromPublicNetworkStats;
 
 import static libcore.io.IoUtils.closeQuietly;
 
@@ -210,7 +211,6 @@
 import com.android.internal.os.StoragedUidIoStatsReader;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.FrameworkStatsLog;
-import com.android.net.module.util.NetworkStatsUtils;
 import com.android.role.RoleManagerLocal;
 import com.android.server.BinderCallsStatsService;
 import com.android.server.LocalManagerRegistry;
@@ -1587,14 +1587,14 @@
                 getNetworkStatsManager().querySummary(template, startTime, endTime);
 
         final NetworkStats nonTaggedStats =
-                NetworkStatsUtils.fromPublicNetworkStats(queryNonTaggedStats);
+                fromPublicNetworkStats(queryNonTaggedStats);
         queryNonTaggedStats.close();
         if (!includeTags) return nonTaggedStats;
 
         final android.app.usage.NetworkStats queryTaggedStats =
                 getNetworkStatsManager().queryTaggedSummary(template, startTime, endTime);
         final NetworkStats taggedStats =
-                NetworkStatsUtils.fromPublicNetworkStats(queryTaggedStats);
+                fromPublicNetworkStats(queryTaggedStats);
         queryTaggedStats.close();
         return nonTaggedStats.add(taggedStats);
     }
diff --git a/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsUtils.java b/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsUtils.java
new file mode 100644
index 0000000..de58852
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsUtils.java
@@ -0,0 +1,109 @@
+/*
+ * 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.stats.pull.netstats;
+
+import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.ROAMING_ALL;
+import static android.net.NetworkStats.SET_ALL;
+
+import android.app.usage.NetworkStats;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Utility methods for accessing {@link android.net.NetworkStats}.
+ */
+public class NetworkStatsUtils {
+
+    /**
+     * Convert structure from android.app.usage.NetworkStats to android.net.NetworkStats.
+     */
+    public static android.net.NetworkStats fromPublicNetworkStats(
+            NetworkStats publiceNetworkStats) {
+        android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
+        while (publiceNetworkStats.hasNextBucket()) {
+            NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+            publiceNetworkStats.getNextBucket(bucket);
+            final android.net.NetworkStats.Entry entry = fromBucket(bucket);
+            stats = stats.addEntry(entry);
+        }
+        return stats;
+    }
+
+    /**
+     * Convert structure from android.app.usage.NetworkStats.Bucket
+     * to android.net.NetworkStats.Entry.
+     */
+    @VisibleForTesting
+    public static android.net.NetworkStats.Entry fromBucket(NetworkStats.Bucket bucket) {
+        return new android.net.NetworkStats.Entry(
+                null /* IFACE_ALL */, bucket.getUid(), convertBucketState(bucket.getState()),
+                convertBucketTag(bucket.getTag()), convertBucketMetered(bucket.getMetered()),
+                convertBucketRoaming(bucket.getRoaming()),
+                convertBucketDefaultNetworkStatus(bucket.getDefaultNetworkStatus()),
+                bucket.getRxBytes(), bucket.getRxPackets(),
+                bucket.getTxBytes(), bucket.getTxPackets(), 0 /* operations */);
+    }
+
+    private static int convertBucketState(int networkStatsSet) {
+        switch (networkStatsSet) {
+            case NetworkStats.Bucket.STATE_ALL: return SET_ALL;
+            case NetworkStats.Bucket.STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
+            case NetworkStats.Bucket.STATE_FOREGROUND:
+                return android.net.NetworkStats.SET_FOREGROUND;
+        }
+        return 0;
+    }
+
+    private static int convertBucketTag(int tag) {
+        switch (tag) {
+            case NetworkStats.Bucket.TAG_NONE: return android.net.NetworkStats.TAG_NONE;
+        }
+        return tag;
+    }
+
+    private static int convertBucketMetered(int metered) {
+        switch (metered) {
+            case NetworkStats.Bucket.METERED_ALL: return METERED_ALL;
+            case NetworkStats.Bucket.METERED_NO: return android.net.NetworkStats.METERED_NO;
+            case NetworkStats.Bucket.METERED_YES: return android.net.NetworkStats.METERED_YES;
+        }
+        return 0;
+    }
+
+    private static int convertBucketRoaming(int roaming) {
+        switch (roaming) {
+            case NetworkStats.Bucket.ROAMING_ALL: return ROAMING_ALL;
+            case NetworkStats.Bucket.ROAMING_NO: return android.net.NetworkStats.ROAMING_NO;
+            case NetworkStats.Bucket.ROAMING_YES: return android.net.NetworkStats.ROAMING_YES;
+        }
+        return 0;
+    }
+
+    private static int convertBucketDefaultNetworkStatus(int defaultNetworkStatus) {
+        switch (defaultNetworkStatus) {
+            case NetworkStats.Bucket.DEFAULT_NETWORK_ALL:
+                return DEFAULT_NETWORK_ALL;
+            case NetworkStats.Bucket.DEFAULT_NETWORK_NO:
+                return android.net.NetworkStats.DEFAULT_NETWORK_NO;
+            case NetworkStats.Bucket.DEFAULT_NETWORK_YES:
+                return android.net.NetworkStats.DEFAULT_NETWORK_YES;
+        }
+        return 0;
+    }
+}
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/uri/TEST_MAPPING b/services/core/java/com/android/server/uri/TEST_MAPPING
index b42d154..0d756bb 100644
--- a/services/core/java/com/android/server/uri/TEST_MAPPING
+++ b/services/core/java/com/android/server/uri/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
     "presubmit": [
         {
-            "name": "FrameworksServicesTests",
-            "options": [
-                {
-                    "include-filter": "com.android.server.uri."
-                }
-            ]
+            "name": "FrameworksServicesTests_android_server_uri"
         },
         {
             "name": "CtsStorageHostTestCases",
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/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 5e03066..2f74a9d 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -124,8 +124,6 @@
 import static com.android.server.wm.ActivityTaskSupervisor.REMOVE_FROM_RECENTS;
 import static com.android.server.wm.BackgroundActivityStartController.BalVerdict;
 import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_DONT_LOCK;
-import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
-import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_ONLY;
 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS;
 import static com.android.server.wm.Task.REPARENT_KEEP_ROOT_TASK_AT_FRONT;
@@ -241,7 +239,6 @@
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
-import android.view.IRecentsAnimationRunner;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationDefinition;
 import android.view.WindowManager;
@@ -1773,16 +1770,13 @@
     }
 
     /**
-     * Start the recents activity to perform the recents animation.
+     * Preload the recents activity.
      *
-     * @param intent                 The intent to start the recents activity.
-     * @param eventTime              When the (touch) event is triggered to start recents activity.
-     * @param recentsAnimationRunner Pass {@code null} to only preload the activity.
+     * @param intent The intent to preload the recents activity.
      */
     @Override
-    public void startRecentsActivity(Intent intent, long eventTime,
-            @Nullable IRecentsAnimationRunner recentsAnimationRunner) {
-        enforceTaskPermission("startRecentsActivity()");
+    public void preloadRecentsActivity(Intent intent) {
+        enforceTaskPermission("preloadRecentsActivity()");
         final int callingPid = Binder.getCallingPid();
         final int callingUid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
@@ -1793,15 +1787,10 @@
                 final int recentsUid = mRecentTasks.getRecentsComponentUid();
                 final WindowProcessController caller = getProcessController(callingPid, callingUid);
 
-                // Start a new recents animation
                 final RecentsAnimation anim = new RecentsAnimation(this, mTaskSupervisor,
                         getActivityStartController(), mWindowManager, intent, recentsComponent,
                         recentsFeatureId, recentsUid, caller);
-                if (recentsAnimationRunner == null) {
-                    anim.preloadRecentsActivity();
-                } else {
-                    anim.startRecentsActivity(recentsAnimationRunner, eventTime);
-                }
+                anim.preloadRecentsActivity();
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -2566,23 +2555,6 @@
     }
 
     @Override
-    public void cancelRecentsAnimation(boolean restoreHomeRootTaskPosition) {
-        enforceTaskPermission("cancelRecentsAnimation()");
-        final long callingUid = Binder.getCallingUid();
-        final long origId = Binder.clearCallingIdentity();
-        try {
-            synchronized (mGlobalLock) {
-                // Cancel the recents animation synchronously (do not hold the WM lock)
-                mWindowManager.cancelRecentsAnimation(restoreHomeRootTaskPosition
-                        ? REORDER_MOVE_TO_ORIGINAL_POSITION
-                        : REORDER_KEEP_IN_PLACE, "cancelRecentsAnimation/uid=" + callingUid);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(origId);
-        }
-    }
-
-    @Override
     public void startSystemLockTaskMode(int taskId) {
         enforceTaskPermission("startSystemLockTaskMode");
         // This makes inner call to look as if it was initiated by system.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 86285fb..129931e 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -857,8 +857,8 @@
             return false;
         }
         if (w.mAttrs.type == TYPE_INPUT_METHOD_DIALOG && mImeLayeringTarget != null
-                && !mImeLayeringTarget.isRequestedVisible(ime())
-                && !mImeLayeringTarget.isVisibleRequested()) {
+                && !(mImeLayeringTarget.isRequestedVisible(ime())
+                        && mImeLayeringTarget.isVisibleRequested())) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java
index 36bc846..f2615f7 100644
--- a/services/core/java/com/android/server/wm/TransparentPolicy.java
+++ b/services/core/java/com/android/server/wm/TransparentPolicy.java
@@ -335,6 +335,8 @@
             // Do not enable the policy if the activity can affect display orientation.
             final int orientation = mActivityRecord.getOverrideOrientation();
             return orientation == SCREEN_ORIENTATION_UNSPECIFIED
+                    // This "!condition" is true if the activity is multi-window mode or the
+                    // display ignores requested orientation.
                     || !mActivityRecord.handlesOrientationChangeFromDescendant(orientation);
         }
 
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/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index dab3978..8332b8b 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -46,12 +46,12 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.art.ArtManagerLocal;
+import com.android.server.profcollect.Utils;
 import com.android.server.wm.ActivityMetricsLaunchObserver;
 import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 import java.util.Arrays;
-import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -280,11 +280,7 @@
             return;
         }
 
-        // Sample for a fraction of app launches.
-        int traceFrequency = DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
-                "applaunch_trace_freq", 2);
-        int randomNum = ThreadLocalRandom.current().nextInt(100);
-        if (randomNum < traceFrequency) {
+        if (Utils.withFrequency("applaunch_trace_freq", 2)) {
             BackgroundThread.get().getThreadHandler().post(() -> {
                 try {
                     mIProfcollect.trace_system("applaunch");
@@ -318,12 +314,7 @@
         if (mIProfcollect == null) {
             return;
         }
-        // Sample for a fraction of dex2oat runs.
-        final int traceFrequency =
-            DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
-                "dex2oat_trace_freq", 25);
-        int randomNum = ThreadLocalRandom.current().nextInt(100);
-        if (randomNum < traceFrequency) {
+        if (Utils.withFrequency("dex2oat_trace_freq", 25)) {
             // Dex2oat could take a while before it starts. Add a short delay before start tracing.
             BackgroundThread.get().getThreadHandler().postDelayed(() -> {
                 try {
@@ -393,27 +384,22 @@
                 if (Arrays.asList(cameraSkipPackages).contains(packageId)) {
                     return;
                 }
-                // Sample for a fraction of camera events.
-                final int traceFrequency =
-                        DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
-                        "camera_trace_freq", 10);
-                int randomNum = ThreadLocalRandom.current().nextInt(100);
-                if (randomNum >= traceFrequency) {
-                    return;
-                }
-                final int traceDuration = 5000;
-                final String traceTag = "camera";
-                BackgroundThread.get().getThreadHandler().post(() -> {
-                    if (mIProfcollect == null) {
-                        return;
-                    }
-                    try {
-                        mIProfcollect.trace_process(traceTag, "android.hardware.camera.provider",
+                if (Utils.withFrequency("camera_trace_freq", 10)) {
+                    final int traceDuration = 5000;
+                    final String traceTag = "camera";
+                    BackgroundThread.get().getThreadHandler().post(() -> {
+                        if (mIProfcollect == null) {
+                            return;
+                        }
+                        try {
+                            mIProfcollect.trace_process(traceTag,
+                                "android.hardware.camera.provider",
                                 traceDuration);
-                    } catch (RemoteException e) {
-                        Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
-                    }
-                });
+                        } catch (RemoteException e) {
+                            Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
+                        }
+                    });
+                }
             }
         }, null);
     }
diff --git a/services/profcollect/src/com/android/server/profcollect/Utils.java b/services/profcollect/src/com/android/server/profcollect/Utils.java
new file mode 100644
index 0000000..d5ef14c
--- /dev/null
+++ b/services/profcollect/src/com/android/server/profcollect/Utils.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.profcollect;
+
+import android.provider.DeviceConfig;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+public final class Utils {
+
+  public static boolean withFrequency(String configName, int defaultFrequency) {
+        int threshold = DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, configName, defaultFrequency);
+        int randomNum = ThreadLocalRandom.current().nextInt(100);
+        return randomNum < threshold;
+    }
+
+}
\ No newline at end of file
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/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
index 53e3143..115cdf6 100644
--- a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
@@ -74,6 +74,8 @@
 import java.util.Arrays;
 import java.util.Map;
 import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
 /**
@@ -221,6 +223,7 @@
     };
 
     public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper {
+        public RuntimeException exception;
         public EnergyConsumerResult[] energyConsumerResults;
         public EnergyMeasurement[] energyMeasurements;
 
@@ -243,6 +246,9 @@
 
         @Override
         public StateResidencyResult[] getStateResidency(int[] powerEntityIds) {
+            if (exception != null) {
+                throw exception;
+            }
             StateResidencyResult[] stateResidencyResultList =
                     new StateResidencyResult[POWER_ENTITY_COUNT];
             for (int i = 0; i < stateResidencyResultList.length; i++) {
@@ -294,6 +300,9 @@
 
         @Override
         public EnergyConsumerResult[] getEnergyConsumed(int[] energyConsumerIds) {
+            if (exception != null) {
+                throw exception;
+            }
             return energyConsumerResults;
         }
 
@@ -322,6 +331,9 @@
 
         @Override
         public EnergyMeasurement[] readEnergyMeter(int[] channelIds) {
+            if (exception != null) {
+                throw exception;
+            }
             return energyMeasurements;
         }
 
@@ -1222,4 +1234,31 @@
         assertThrows(NullPointerException.class, () -> iPowerStatsService.getPowerMonitorReadings(
                 new int[] {0}, null));
     }
+
+    @Test
+    public void getEnergyConsumedAsync_halException() {
+        mPowerStatsHALWrapper.exception = new IllegalArgumentException();
+        CompletableFuture<EnergyConsumerResult[]> future =
+                mService.getPowerStatsInternal().getEnergyConsumedAsync(new int[]{1});
+        ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+        assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void getStateResidencyAsync_halException() {
+        mPowerStatsHALWrapper.exception = new IllegalArgumentException();
+        CompletableFuture<StateResidencyResult[]> future =
+                mService.getPowerStatsInternal().getStateResidencyAsync(new int[]{1});
+        ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+        assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void readEnergyMeterAsync_halException() {
+        mPowerStatsHALWrapper.exception = new IllegalArgumentException();
+        CompletableFuture<EnergyMeasurement[]> future =
+                mService.getPowerStatsInternal().readEnergyMeterAsync(new int[]{1});
+        ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+        assertThat(exception.getCause()).isInstanceOf(IllegalArgumentException.class);
+    }
 }
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 701c350..09f81f7 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -36,8 +36,8 @@
         "-Werror",
     ],
     static_libs: [
-        "a11ychecker-protos-java-proto-lite",
         "aatf",
+        "accessibility_protos_lite",
         "cts-input-lib",
         "frameworks-base-testutils",
         "services.accessibility",
@@ -85,6 +85,7 @@
         "securebox",
         "flag-junit",
         "ravenwood-junit",
+        "net-tests-utils",
         "net_flags_lib",
         "CtsVirtualDeviceCommonLib",
         "com_android_server_accessibility_flags_lib",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java
index c1b3929..5ee86ffa 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManagerTest.java
@@ -23,7 +23,7 @@
 import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME;
 import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME;
 import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_DEFAULT_BROWSER;
-import static com.android.server.accessibility.a11ychecker.TestUtils.createAtom;
+import static com.android.server.accessibility.a11ychecker.TestUtils.createResult;
 import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -32,6 +32,8 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.platform.test.annotations.DisableFlags;
@@ -112,19 +114,19 @@
                         .setViewIdResourceName("node2")
                         .build();
 
-        Set<A11yCheckerProto.AccessibilityCheckResultReported> results =
+        Set<AndroidAccessibilityCheckerResult> results =
                 mAccessibilityCheckerManager.maybeRunA11yChecker(
                         List.of(mockNodeInfo1, mockNodeInfo2), QUALIFIED_TEST_ACTIVITY_NAME,
                         new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
                                 TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0);
 
         assertThat(results).containsExactly(
-                createAtom(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME,
-                        A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
-                        A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2),
-                createAtom(/*viewIdResourceName=*/ "node2", TEST_ACTIVITY_NAME,
-                        A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
-                        A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2)
+                createResult(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME,
+                        AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+                        AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE, /*resultId=*/ 2),
+                createResult(/*viewIdResourceName=*/ "node2", TEST_ACTIVITY_NAME,
+                        AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+                        AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE, /*resultId=*/ 2)
         );
     }
 
@@ -137,7 +139,7 @@
                         .setViewIdResourceName("node1")
                         .build();
 
-        Set<A11yCheckerProto.AccessibilityCheckResultReported> results =
+        Set<AndroidAccessibilityCheckerResult> results =
                 mAccessibilityCheckerManager.maybeRunA11yChecker(
                         List.of(mockNodeInfo), QUALIFIED_TEST_ACTIVITY_NAME,
                         new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
@@ -158,16 +160,17 @@
                         .setViewIdResourceName("node1")
                         .build();
 
-        Set<A11yCheckerProto.AccessibilityCheckResultReported> results =
+        Set<AndroidAccessibilityCheckerResult> results =
                 mAccessibilityCheckerManager.maybeRunA11yChecker(
                         List.of(mockNodeInfo, mockNodeInfoDuplicate), QUALIFIED_TEST_ACTIVITY_NAME,
                         new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
                                 TEST_A11Y_SERVICE_CLASS_NAME), /*userId=*/ 0);
 
         assertThat(results).containsExactly(
-                createAtom(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME,
-                        A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
-                        A11yCheckerProto.AccessibilityCheckResultType.ERROR, /*resultId=*/ 2)
+                createResult(/*viewIdResourceName=*/ "node1", TEST_ACTIVITY_NAME,
+                        AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+                        AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE, /*resultId=*/
+                        2)
         );
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
index 5b4e72e..4ec2fb9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
@@ -21,13 +21,15 @@
 import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME;
 import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME;
 import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
-import static com.android.server.accessibility.a11ychecker.TestUtils.createAtom;
+import static com.android.server.accessibility.a11ychecker.TestUtils.createResult;
 import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.when;
 
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -87,7 +89,7 @@
                         AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN, null, 5,
                         null);
 
-        Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+        Set<AndroidAccessibilityCheckerResult> results =
                 AccessibilityCheckerUtils.processResults(
                         mockNodeInfo,
                         List.of(result1, result2, result3, result4),
@@ -96,13 +98,13 @@
                         new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
                                 TEST_A11Y_SERVICE_CLASS_NAME));
 
-        assertThat(atoms).containsExactly(
-                createAtom("TargetNode", "",
-                        A11yCheckerProto.AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK,
-                        A11yCheckerProto.AccessibilityCheckResultType.WARNING, 1),
-                createAtom("TargetNode", "",
-                        A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
-                        A11yCheckerProto.AccessibilityCheckResultType.ERROR, 2)
+        assertThat(results).containsExactly(
+                createResult("TargetNode", "",
+                        AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK,
+                        AccessibilityCheckResultType.WARNING_CHECK_RESULT_TYPE, 1),
+                createResult("TargetNode", "",
+                        AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+                        AccessibilityCheckResultType.ERROR_CHECK_RESULT_TYPE, 2)
         );
     }
 
@@ -126,7 +128,7 @@
                         TouchTargetSizeCheck.class,
                         AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null);
 
-        Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+        Set<AndroidAccessibilityCheckerResult> results =
                 AccessibilityCheckerUtils.processResults(
                         mockNodeInfo,
                         List.of(result1, result2),
@@ -135,7 +137,7 @@
                         new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
                                 TEST_A11Y_SERVICE_CLASS_NAME));
 
-        assertThat(atoms).isEmpty();
+        assertThat(results).isEmpty();
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
index acf64b6..8e0b2ed 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
@@ -20,6 +20,8 @@
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.when;
 
+import android.accessibility.AccessibilityCheckClass;
+import android.accessibility.AccessibilityCheckResultType;
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
@@ -90,20 +92,20 @@
         return accessibilityEvent;
     }
 
-    static A11yCheckerProto.AccessibilityCheckResultReported createAtom(
+    static AndroidAccessibilityCheckerResult createResult(
             String viewIdResourceName,
             String activityName,
-            A11yCheckerProto.AccessibilityCheckClass checkClass,
-            A11yCheckerProto.AccessibilityCheckResultType resultType,
+            AccessibilityCheckClass checkClass,
+            AccessibilityCheckResultType resultType,
             int resultId) {
-        return A11yCheckerProto.AccessibilityCheckResultReported.newBuilder()
+        return AndroidAccessibilityCheckerResult.newBuilder()
                 .setPackageName(TEST_APP_PACKAGE_NAME)
                 .setAppVersionCode(TEST_APP_VERSION_CODE)
                 .setUiElementPath(TEST_APP_PACKAGE_NAME + ":" + viewIdResourceName)
                 .setWindowTitle(TEST_WINDOW_TITLE)
                 .setActivityName(activityName)
                 .setSourceComponentName(new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
-                        TEST_A11Y_SERVICE_CLASS_NAME).flattenToString())
+                        TEST_A11Y_SERVICE_CLASS_NAME))
                 .setSourceVersionCode(TEST_A11Y_SERVICE_SOURCE_VERSION_CODE)
                 .setResultCheckClass(checkClass)
                 .setResultType(resultType)
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 2b93ccb..a7e8a00 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -2148,6 +2148,40 @@
                 .hasSize(1);
     }
 
+
+    @Test
+    public void handleReportAudioStatus_SamOnAvrStandby_startSystemAudioActionFromTv() {
+        mHdmiControlService.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
+                HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
+        // Emulate Audio device on port 0x1000 (does not support ARC)
+        mNativeWrapper.setPortConnectionStatus(1, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        HdmiCecMessage reportPowerStatus =
+                HdmiCecMessageBuilder.buildReportPowerStatus(ADDR_AUDIO_SYSTEM, ADDR_TV,
+                        HdmiControlManager.POWER_STATUS_STANDBY);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mNativeWrapper.onCecMessage(reportPowerStatus);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).hasSize(0);
+
+        HdmiCecFeatureAction systemAudioAutoInitiationAction =
+                new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM);
+        mHdmiCecLocalDeviceTv.addAndStartAction(systemAudioAutoInitiationAction);
+        HdmiCecMessage reportSystemAudioMode =
+                HdmiCecMessageBuilder.buildReportSystemAudioMode(
+                        ADDR_AUDIO_SYSTEM,
+                        mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(),
+                        true);
+        mHdmiControlService.handleCecCommand(reportSystemAudioMode);
+        mTestLooper.dispatchAll();
+
+        // SAM must be on; ARC must be off
+        assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).hasSize(1);
+    }
+
     protected static class MockTvDevice extends HdmiCecLocalDeviceTv {
         MockTvDevice(HdmiControlService service) {
             super(service);
diff --git a/services/tests/servicestests/src/com/android/server/stats/pull/netstats/NetworkStatsUtilsTest.kt b/services/tests/servicestests/src/com/android/server/stats/pull/netstats/NetworkStatsUtilsTest.kt
new file mode 100644
index 0000000..c560c04
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/stats/pull/netstats/NetworkStatsUtilsTest.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.stats.pull.netstats
+
+import android.net.NetworkStats
+import android.net.NetworkStats.DEFAULT_NETWORK_NO
+import android.net.NetworkStats.DEFAULT_NETWORK_YES
+import android.net.NetworkStats.Entry
+import android.net.NetworkStats.METERED_NO
+import android.net.NetworkStats.ROAMING_NO
+import android.net.NetworkStats.ROAMING_YES
+import android.net.NetworkStats.SET_DEFAULT
+import android.net.NetworkStats.TAG_NONE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.testutils.assertEntryEquals
+import com.android.testutils.assertNetworkStatsEquals
+import com.android.testutils.makePublicStatsFromAndroidNetStats
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:NetworkStatsUtilsTest
+ */
+@RunWith(AndroidJUnit4::class)
+class NetworkStatsUtilsTest {
+
+    @Test
+    fun testBucketToEntry() {
+        val bucket = makeMockBucket(android.app.usage.NetworkStats.Bucket.UID_ALL,
+                android.app.usage.NetworkStats.Bucket.TAG_NONE,
+                android.app.usage.NetworkStats.Bucket.STATE_DEFAULT,
+                android.app.usage.NetworkStats.Bucket.METERED_YES,
+                android.app.usage.NetworkStats.Bucket.ROAMING_NO,
+                android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12)
+        val entry = NetworkStatsUtils.fromBucket(bucket)
+        val expectedEntry = NetworkStats.Entry(null /* IFACE_ALL */, NetworkStats.UID_ALL,
+                NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
+                NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12,
+                0 /* operations */)
+
+        assertEntryEquals(expectedEntry, entry)
+    }
+
+    @Test
+    fun testPublicStatsToAndroidNetStats() {
+        val uid1 = 10001
+        val uid2 = 10002
+        val testIface = "wlan0"
+        val testAndroidNetStats = NetworkStats(0L, 3)
+                .addEntry(Entry(testIface, uid1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 3))
+                .addEntry(Entry(
+                        testIface, uid2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 2, 7, 2, 5, 7))
+                .addEntry(Entry(testIface, uid2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 4, 5, 3, 1, 8))
+        val publicStats: android.app.usage.NetworkStats =
+                makePublicStatsFromAndroidNetStats(testAndroidNetStats)
+        val androidNetStats: NetworkStats =
+                NetworkStatsUtils.fromPublicNetworkStats(publicStats)
+
+        // 1. The public `NetworkStats` class does not include interface information.
+        //    Interface details must be removed and items with duplicated
+        //    keys need to be merged before making any comparisons.
+        // 2. The public `NetworkStats` class lacks an operations field.
+        //    Thus, the information will not be preserved during the conversion.
+        val expectedStats = NetworkStats(0L, 2)
+                .addEntry(Entry(null, uid1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 0))
+                .addEntry(Entry(null, uid2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 6, 12, 5, 6, 0))
+        assertNetworkStatsEquals(expectedStats, androidNetStats)
+    }
+
+    private fun makeMockBucket(
+            uid: Int,
+            tag: Int,
+            state: Int,
+            metered: Int,
+            roaming: Int,
+            defaultNetwork: Int,
+            rxBytes: Long,
+            rxPackets: Long,
+            txBytes: Long,
+            txPackets: Long
+    ): android.app.usage.NetworkStats.Bucket {
+        val ret: android.app.usage.NetworkStats.Bucket =
+                mock(android.app.usage.NetworkStats.Bucket::class.java)
+        doReturn(uid).`when`(ret).getUid()
+        doReturn(tag).`when`(ret).getTag()
+        doReturn(state).`when`(ret).getState()
+        doReturn(metered).`when`(ret).getMetered()
+        doReturn(roaming).`when`(ret).getRoaming()
+        doReturn(defaultNetwork).`when`(ret).getDefaultNetworkStatus()
+        doReturn(rxBytes).`when`(ret).getRxBytes()
+        doReturn(rxPackets).`when`(ret).getRxPackets()
+        doReturn(txBytes).`when`(ret).getTxBytes()
+        doReturn(txPackets).`when`(ret).getTxPackets()
+        return ret
+    }
+}
\ No newline at end of file
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 fee9c6c..c788f3b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -134,11 +134,6 @@
                 isUnresizable);
     }
 
-    void configureTopActivityIgnoreOrientationRequest(boolean ignoreOrientationRequest) {
-        mActivityStack.top().mDisplayContent
-                .setIgnoreOrientationRequest(ignoreOrientationRequest);
-    }
-
     void configureUnresizableTopActivity(@ActivityInfo.ScreenOrientation int screenOrientation) {
         configureTopActivity(/* minAspect */ -1, /* maxAspect */ -1, screenOrientation,
                 /* isUnresizable */ true);
@@ -248,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/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index ec5e51e..58e919d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2796,15 +2796,17 @@
         final WindowState imeAppTarget =
                 createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget");
         mDisplayContent.setImeLayeringTarget(imeAppTarget);
-        spyOn(imeAppTarget);
-        doReturn(true).when(imeAppTarget).isRequestedVisible(ime());
+        imeAppTarget.setRequestedVisibleTypes(ime());
         assertEquals(imeMenuDialog, mDisplayContent.findFocusedWindow());
 
         // Verify imeMenuDialog doesn't be focused window if the next IME target is closing.
         final WindowState nextImeAppTarget =
                 createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "nextImeAppTarget");
         makeWindowVisibleAndDrawn(nextImeAppTarget);
-        nextImeAppTarget.mActivityRecord.commitVisibility(false, false);
+        // Even if the app still requests IME, the ime dialog should not gain focus if the target
+        // app is invisible.
+        nextImeAppTarget.setRequestedVisibleTypes(ime());
+        nextImeAppTarget.mActivityRecord.setVisibility(false);
         mDisplayContent.setImeLayeringTarget(nextImeAppTarget);
         assertNotEquals(imeMenuDialog, mDisplayContent.findFocusedWindow());
     }
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/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index b95f621..8f3d3c5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -1481,9 +1481,6 @@
         assertSecurityException(expectCallable,
                 () -> mAtm.unregisterTaskStackListener(null));
         assertSecurityException(expectCallable, () -> mAtm.cancelTaskWindowTransition(0));
-        assertSecurityException(expectCallable, () -> mAtm.startRecentsActivity(null, 0,
-                null));
-        assertSecurityException(expectCallable, () -> mAtm.cancelRecentsAnimation(true));
         assertSecurityException(expectCallable, () -> mAtm.stopAppSwitches());
         assertSecurityException(expectCallable, () -> mAtm.resumeAppSwitches());
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
index da437c4..73d1031 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
@@ -19,31 +19,19 @@
 import static android.app.ActivityManager.PROCESS_STATE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.server.wm.ActivityRecord.State.PAUSED;
 import static com.android.server.wm.ActivityRecord.State.STOPPING;
-import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 
 import android.content.ComponentName;
@@ -51,12 +39,9 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.platform.test.annotations.Presubmit;
-import android.view.IRecentsAnimationRunner;
 
 import androidx.test.filters.MediumTest;
 
-import com.android.server.wm.RecentsAnimationController.RecentsAnimationCallbacks;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,61 +55,17 @@
 @RunWith(WindowTestRunner.class)
 public class RecentsAnimationTest extends WindowTestsBase {
 
-    private static final int TEST_USER_ID = 100;
-
     private final ComponentName mRecentsComponent =
             new ComponentName(mContext.getPackageName(), "RecentsActivity");
-    private RecentsAnimationController mRecentsAnimationController;
 
     @Before
     public void setUp() throws Exception {
-        mRecentsAnimationController = mock(RecentsAnimationController.class);
-        mAtm.mWindowManager.setRecentsAnimationController(mRecentsAnimationController);
-        doNothing().when(mAtm.mWindowManager).initializeRecentsAnimation(
-                anyInt(), any(), any(), anyInt(), any(), any());
-
         final RecentTasks recentTasks = mAtm.getRecentTasks();
         spyOn(recentTasks);
         doReturn(mRecentsComponent).when(recentTasks).getRecentsComponent();
     }
 
     @Test
-    public void testRecentsActivityVisiblility() {
-        TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
-        Task recentsStack = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_RECENTS, true /* onTop */);
-        final WindowProcessController wpc = mSystemServicesTestRule.addProcess(
-                mRecentsComponent.getPackageName(), mRecentsComponent.getPackageName(),
-                // Use real pid/uid of the test so the corresponding process can be mapped by
-                // Binder.getCallingPid/Uid.
-                WindowManagerService.MY_PID, WindowManagerService.MY_UID);
-        ActivityRecord recentActivity = new ActivityBuilder(mAtm)
-                .setComponent(mRecentsComponent)
-                .setTask(recentsStack)
-                .setUseProcess(wpc)
-                .build();
-        ActivityRecord topActivity = new ActivityBuilder(mAtm).setCreateTask(true).build();
-        topActivity.getRootTask().moveToFront("testRecentsActivityVisiblility");
-
-        doCallRealMethod().when(mRootWindowContainer).ensureActivitiesVisible(
-                any() /* starting */, anyBoolean() /* notifyClients */);
-
-        RecentsAnimationCallbacks recentsAnimation = startRecentsActivity(
-                mRecentsComponent, true /* getRecentsAnimation */);
-        // The launch-behind state should make the recents activity visible.
-        assertTrue(recentActivity.isVisibleRequested());
-        assertEquals(ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS,
-                mAtm.mDemoteTopAppReasons);
-        assertFalse(mAtm.mInternal.useTopSchedGroupForTopProcess());
-
-        // Simulate the animation is cancelled without changing the stack order.
-        recentsAnimation.onAnimationFinished(REORDER_KEEP_IN_PLACE, false /* sendUserLeaveHint */);
-        // The non-top recents activity should be invisible by the restored launch-behind state.
-        assertFalse(recentActivity.isVisibleRequested());
-        assertEquals(0, mAtm.mDemoteTopAppReasons);
-    }
-
-    @Test
     public void testPreloadRecentsActivity() {
         TaskDisplayArea defaultTaskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
         final Task homeStack =
@@ -155,8 +96,7 @@
 
         Intent recentsIntent = new Intent().setComponent(mRecentsComponent);
         // Null animation indicates to preload.
-        mAtm.startRecentsActivity(recentsIntent, 0 /* eventTime */,
-                null /* recentsAnimationRunner */);
+        mAtm.preloadRecentsActivity(recentsIntent);
 
         Task recentsStack = defaultTaskDisplayArea.getRootTask(WINDOWING_MODE_FULLSCREEN,
                 ACTIVITY_TYPE_RECENTS);
@@ -168,223 +108,10 @@
         assertThat(recentsActivity.getState()).isEqualTo(STOPPING);
         assertFalse(recentsActivity.isVisibleRequested());
 
-        // Assume it is stopped to test next use case.
-        recentsActivity.activityStopped(null /* newIcicle */, null /* newPersistentState */,
-                null /* description */);
-
         spyOn(recentsActivity);
-        // Start when the recents activity exists. It should ensure the configuration.
-        mAtm.startRecentsActivity(recentsIntent, 0 /* eventTime */,
-                null /* recentsAnimationRunner */);
+        // Preload when the recents activity exists. It should ensure the configuration.
+        mAtm.preloadRecentsActivity(recentsIntent);
 
         verify(recentsActivity).ensureActivityConfiguration(eq(true) /* ignoreVisibility */);
     }
-
-    @Test
-    public void testRestartRecentsActivity() throws Exception {
-        // Have a recents activity that is not attached to its process (ActivityRecord.app = null).
-        TaskDisplayArea defaultTaskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
-        Task recentsStack = defaultTaskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_RECENTS, true /* onTop */);
-        ActivityRecord recentActivity = new ActivityBuilder(mAtm).setComponent(
-                mRecentsComponent).setCreateTask(true).setParentTask(recentsStack).build();
-        WindowProcessController app = recentActivity.app;
-        recentActivity.app = null;
-
-        // Start an activity on top.
-        new ActivityBuilder(mAtm).setCreateTask(true).build().getRootTask().moveToFront(
-                "testRestartRecentsActivity");
-
-        doCallRealMethod().when(mRootWindowContainer).ensureActivitiesVisible(
-                any() /* starting */, anyBoolean() /* notifyClients */);
-        doReturn(app).when(mAtm).getProcessController(eq(recentActivity.processName), anyInt());
-        doNothing().when(mClientLifecycleManager).scheduleTransaction(any());
-
-        startRecentsActivity();
-
-        // Recents activity must be restarted, but not be resumed while running recents animation.
-        verify(mRootWindowContainer.mTaskSupervisor).startSpecificActivity(
-                eq(recentActivity), eq(false), anyBoolean());
-        assertThat(recentActivity.getState()).isEqualTo(PAUSED);
-    }
-
-    @Test
-    public void testSetLaunchTaskBehindOfTargetActivity() {
-        TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
-        Task homeStack = taskDisplayArea.getRootHomeTask();
-        // Assume the home activity support recents.
-        ActivityRecord targetActivity = homeStack.getTopNonFinishingActivity();
-        if (targetActivity == null) {
-            targetActivity = new ActivityBuilder(mAtm)
-                    .setCreateTask(true)
-                    .setParentTask(homeStack)
-                    .build();
-        }
-
-        // Put another home activity in home stack.
-        ActivityRecord anotherHomeActivity = new ActivityBuilder(mAtm)
-                .setComponent(new ComponentName(mContext.getPackageName(), "Home2"))
-                .setCreateTask(true)
-                .setParentTask(homeStack)
-                .build();
-        // Start an activity on top so the recents activity can be started.
-        new ActivityBuilder(mAtm)
-                .setCreateTask(true)
-                .build()
-                .getRootTask()
-                .moveToFront("Activity start");
-
-        // Start the recents animation.
-        RecentsAnimationCallbacks recentsAnimation = startRecentsActivity(
-                targetActivity.getTask().getBaseIntent().getComponent(),
-                true /* getRecentsAnimation */);
-        // Ensure launch-behind is set for being visible.
-        assertTrue(targetActivity.mLaunchTaskBehind);
-
-        anotherHomeActivity.moveFocusableActivityToTop("launchAnotherHome");
-
-        // The test uses mocked RecentsAnimationController so we have to invoke the callback
-        // manually to simulate the flow.
-        recentsAnimation.onAnimationFinished(REORDER_KEEP_IN_PLACE, false /* sendUserLeaveHint */);
-        // We should restore the launch-behind of the original target activity.
-        assertFalse(targetActivity.mLaunchTaskBehind);
-    }
-
-    @Test
-    public void testCancelAnimationOnVisibleStackOrderChange() {
-        TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
-        Task fullscreenStack = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_STANDARD, true /* onTop */);
-        new ActivityBuilder(mAtm)
-                .setComponent(new ComponentName(mContext.getPackageName(), "App1"))
-                .setCreateTask(true)
-                .setParentTask(fullscreenStack)
-                .build();
-        Task recentsStack = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_RECENTS, true /* onTop */);
-        new ActivityBuilder(mAtm)
-                .setComponent(mRecentsComponent)
-                .setCreateTask(true)
-                .setParentTask(recentsStack)
-                .build();
-        Task fullscreenStack2 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_STANDARD, true /* onTop */);
-        new ActivityBuilder(mAtm)
-                .setComponent(new ComponentName(mContext.getPackageName(), "App2"))
-                .setCreateTask(true)
-                .setParentTask(fullscreenStack2)
-                .build();
-
-        // Start the recents animation
-        startRecentsActivity();
-
-        fullscreenStack.moveToFront("Activity start");
-
-        // Assume recents animation already started, set a state that cancel recents animation
-        // with screenshot.
-        doReturn(true).when(mRecentsAnimationController).shouldDeferCancelUntilNextTransition();
-        doReturn(true).when(mRecentsAnimationController).shouldDeferCancelWithScreenshot();
-        // Start another fullscreen activity.
-        fullscreenStack2.moveToFront("Activity start");
-
-        // Ensure that the recents animation was canceled by setCancelOnNextTransitionStart().
-        verify(mRecentsAnimationController, times(1)).setCancelOnNextTransitionStart();
-    }
-
-    @Test
-    public void testKeepAnimationOnHiddenStackOrderChange() {
-        TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
-        Task fullscreenStack = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_STANDARD, true /* onTop */);
-        new ActivityBuilder(mAtm)
-                .setComponent(new ComponentName(mContext.getPackageName(), "App1"))
-                .setCreateTask(true)
-                .setParentTask(fullscreenStack)
-                .build();
-        Task recentsStack = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_RECENTS, true /* onTop */);
-        new ActivityBuilder(mAtm)
-                .setComponent(mRecentsComponent)
-                .setCreateTask(true)
-                .setParentTask(recentsStack)
-                .build();
-        Task fullscreenStack2 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_STANDARD, true /* onTop */);
-        new ActivityBuilder(mAtm)
-                .setComponent(new ComponentName(mContext.getPackageName(), "App2"))
-                .setCreateTask(true)
-                .setParentTask(fullscreenStack2)
-                .build();
-
-        // Start the recents animation
-        startRecentsActivity();
-
-        fullscreenStack.removeIfPossible();
-
-        // Ensure that the recents animation was NOT canceled
-        verify(mAtm.mWindowManager, times(0)).cancelRecentsAnimation(
-                eq(REORDER_KEEP_IN_PLACE), any());
-        verify(mRecentsAnimationController, times(0)).setCancelOnNextTransitionStart();
-    }
-
-    @Test
-    public void testMultipleUserHomeActivity_findUserHomeTask() {
-        TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultDisplay()
-                .getDefaultTaskDisplayArea();
-        Task homeStack = taskDisplayArea.getRootTask(WINDOWING_MODE_UNDEFINED,
-                ACTIVITY_TYPE_HOME);
-        ActivityRecord otherUserHomeActivity = new ActivityBuilder(mAtm)
-                .setParentTask(homeStack)
-                .setCreateTask(true)
-                .setComponent(new ComponentName(mContext.getPackageName(), "Home2"))
-                .build();
-        otherUserHomeActivity.getTask().mUserId = TEST_USER_ID;
-
-        Task fullscreenStack = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_STANDARD, true /* onTop */);
-        new ActivityBuilder(mAtm)
-                .setComponent(new ComponentName(mContext.getPackageName(), "App1"))
-                .setCreateTask(true)
-                .setParentTask(fullscreenStack)
-                .build();
-
-        doReturn(TEST_USER_ID).when(mAtm).getCurrentUserId();
-        doCallRealMethod().when(mRootWindowContainer).ensureActivitiesVisible(
-                any() /* starting */, anyBoolean() /* notifyClients */);
-
-        startRecentsActivity(otherUserHomeActivity.getTask().getBaseIntent().getComponent(),
-                true);
-
-        // Ensure we find the task for the right user and it is made visible
-        assertTrue(otherUserHomeActivity.isVisibleRequested());
-    }
-
-    private void startRecentsActivity() {
-        startRecentsActivity(mRecentsComponent, false /* getRecentsAnimation */);
-    }
-
-    /**
-     * @return non-null {@link RecentsAnimationCallbacks} if the given {@code getRecentsAnimation}
-     *         is {@code true}.
-     */
-    private RecentsAnimationCallbacks startRecentsActivity(ComponentName recentsComponent,
-            boolean getRecentsAnimation) {
-        RecentsAnimationCallbacks[] recentsAnimation = { null };
-        if (getRecentsAnimation) {
-            doAnswer(invocation -> {
-                // The callback is actually RecentsAnimation.
-                recentsAnimation[0] = invocation.getArgument(2);
-                return null;
-            }).when(mAtm.mWindowManager).initializeRecentsAnimation(
-                    anyInt() /* targetActivityType */, any() /* recentsAnimationRunner */,
-                    any() /* callbacks */, anyInt() /* displayId */, any() /* recentTaskIds */,
-                    any() /* targetActivity */);
-        }
-
-        Intent recentsIntent = new Intent();
-        recentsIntent.setComponent(recentsComponent);
-        mAtm.startRecentsActivity(recentsIntent, 0 /* eventTime */,
-                mock(IRecentsAnimationRunner.class));
-        return recentsAnimation[0];
-    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
index a0641cd..29f6360 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
@@ -73,6 +73,7 @@
     public void testPolicyRunningWhenTransparentIsUsed() {
         runTestScenario((robot) -> {
             robot.transparentActivity((ta) -> {
+                ta.activity().setIgnoreOrientationRequest(true);
                 ta.launchTransparentActivityInTask();
 
                 ta.checkTopActivityTransparentPolicyStartNotInvoked();
@@ -85,6 +86,7 @@
     public void testCleanLetterboxConfigListenerWhenTranslucentIsDestroyed() {
         runTestScenario((robot) -> {
             robot.transparentActivity((ta) -> {
+                ta.activity().setIgnoreOrientationRequest(true);
                 ta.launchTransparentActivityInTask();
                 ta.checkTopActivityTransparentPolicyStartNotInvoked();
                 ta.checkTopActivityTransparentPolicyStateIsRunning(/* running */ true);
@@ -102,6 +104,7 @@
     public void testApplyStrategyAgainWhenOpaqueIsDestroyed() {
         runTestScenario((robot) -> {
             robot.transparentActivity((ta) -> {
+                ta.activity().setIgnoreOrientationRequest(true);
                 ta.launchOpaqueActivityInTask();
                 ta.checkTopActivityTransparentPolicyStateIsRunning(/* running */ false);
 
@@ -133,6 +136,7 @@
     public void testNotApplyStrategyAgainWhenOpaqueIsNotDestroyed() {
         runTestScenario((robot) -> {
             robot.transparentActivity((ta) -> {
+                ta.activity().setIgnoreOrientationRequest(true);
                 ta.launchOpaqueActivityInTask();
                 ta.checkTopActivityTransparentPolicyStateIsRunning(/* running */ false);
 
@@ -152,7 +156,7 @@
                 ta.applyOnActivity((a) -> {
                     a.configureTopActivity(/* minAspect */ 1.2f, /* maxAspect */ 1.5f,
                             SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable */ true);
-                    a.configureTopActivityIgnoreOrientationRequest(true);
+                    a.setIgnoreOrientationRequest(true);
                     a.launchActivity(/* minAspect */ 1.1f, /* maxAspect */ 3f,
                             SCREEN_ORIENTATION_LANDSCAPE, /* transparent */true,
                             /* withComponent */ false, /* addToTask */true);
@@ -172,6 +176,7 @@
     public void testApplyStrategyToTransparentActivitiesRetainsWindowConfigurationProperties() {
         runTestScenario((robot) -> {
             robot.transparentActivity((ta) -> {
+                ta.activity().setIgnoreOrientationRequest(true);
                 ta.launchTransparentActivity();
 
                 ta.forceChangeInTopActivityConfiguration();
@@ -186,6 +191,7 @@
     public void testApplyStrategyToMultipleTranslucentActivities() {
         runTestScenario((robot) -> {
             robot.transparentActivity((ta) -> {
+                ta.activity().setIgnoreOrientationRequest(true);
                 ta.launchTransparentActivityInTask();
                 ta.checkTopActivityTransparentPolicyStateIsRunning(/* running */ true);
                 ta.checkTopActivityHasInheritedBoundsFrom(/* fromTop */ 1);
@@ -214,7 +220,7 @@
     @Test
     public void testNotRunStrategyToTranslucentActivitiesIfRespectOrientation() {
         runTestScenario(robot -> robot.transparentActivity(ta -> ta.applyOnActivity((a) -> {
-            a.configureTopActivityIgnoreOrientationRequest(false);
+            a.setIgnoreOrientationRequest(false);
             // The translucent activity is SCREEN_ORIENTATION_PORTRAIT.
             ta.launchTransparentActivityInTask();
             // Though TransparentPolicyState will be started, it won't be considered as running.
@@ -222,7 +228,7 @@
 
             // If the display changes to ignore orientation request, e.g. unfold, the policy should
             // take effect.
-            a.configureTopActivityIgnoreOrientationRequest(true);
+            a.setIgnoreOrientationRequest(true);
             ta.checkTopActivityTransparentPolicyStateIsRunning(/* running */ true);
             ta.setDisplayContentBounds(0, 0, 900, 1800);
             ta.checkTopActivityHasInheritedBoundsFrom(/* fromTop */ 1);
@@ -234,7 +240,7 @@
         runTestScenario((robot) -> {
             robot.transparentActivity((ta) -> {
                 ta.applyOnActivity((a) -> {
-                    a.configureTopActivityIgnoreOrientationRequest(true);
+                    a.setIgnoreOrientationRequest(true);
                     a.configureUnresizableTopActivity(SCREEN_ORIENTATION_PORTRAIT);
                     a.rotateDisplayForTopActivity(ROTATION_90);
                     a.checkTopActivityInSizeCompatMode(/* inScm */ true);
@@ -257,7 +263,7 @@
             robot.transparentActivity((ta) -> {
                 ta.applyOnActivity((a) -> {
                     a.configureUnresizableTopActivity(SCREEN_ORIENTATION_PORTRAIT);
-                    a.configureTopActivityIgnoreOrientationRequest(true);
+                    a.setIgnoreOrientationRequest(true);
                     ta.launchTransparentActivity();
 
                     a.assertFalseOnTopActivity(ActivityRecord::fillsParent);
@@ -284,7 +290,7 @@
                                 .setLetterboxHorizontalPositionMultiplier(1.0f);
                     });
                     a.configureUnresizableTopActivity(SCREEN_ORIENTATION_PORTRAIT);
-                    a.configureTopActivityIgnoreOrientationRequest(true);
+                    a.setIgnoreOrientationRequest(true);
                     ta.launchTransparentActivityInTask();
                     ta.checkTopActivityHasInheritedBoundsFrom(/* fromTop */ 1);
 
@@ -309,7 +315,7 @@
         runTestScenario((robot) -> {
             robot.transparentActivity((ta) -> {
                 ta.applyOnActivity((a) -> {
-                    a.configureTopActivityIgnoreOrientationRequest(true);
+                    a.setIgnoreOrientationRequest(true);
                     a.configureUnresizableTopActivity(SCREEN_ORIENTATION_PORTRAIT);
                     // Rotate to put activity in size compat mode.
                     a.rotateDisplayForTopActivity(ROTATION_90);
diff --git a/services/usage/java/com/android/server/usage/TEST_MAPPING b/services/usage/java/com/android/server/usage/TEST_MAPPING
index 6ceb763..c878054 100644
--- a/services/usage/java/com/android/server/usage/TEST_MAPPING
+++ b/services/usage/java/com/android/server/usage/TEST_MAPPING
@@ -4,15 +4,7 @@
       "name": "FrameworksCoreTests_usage"
     },
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.usage"
-        },
-        {
-          "exclude-filter": "com.android.server.usage.StorageStatsServiceTest"
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_usage"
     },
     {
       "name": "CtsBRSTestCases",
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/TEST_MAPPING b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/TEST_MAPPING
index 9ed894b..509d95e 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/TEST_MAPPING
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/TEST_MAPPING
@@ -1,12 +1,7 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.soundtrigger_middleware"
-        }
-      ]
+      "name": "FrameworksServicesTests_android_server_soundtrigger_middleware"
     }
   ]
 }
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();
     }
diff --git a/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java b/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java
index 80d495d..cb26edc 100644
--- a/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java
+++ b/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java
@@ -30,6 +30,8 @@
  */
 public class ResizeHWLayerActivity extends AppCompatActivity {
 
+    private ValueAnimator mAnimator;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -43,10 +45,10 @@
         PropertyValuesHolder pvhWidth = PropertyValuesHolder.ofInt("width", width, 1);
         PropertyValuesHolder pvhHeight = PropertyValuesHolder.ofInt("height", height, 1);
         final LayoutParams params = child.getLayoutParams();
-        ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder(pvhWidth, pvhHeight);
-        animator.setRepeatMode(ValueAnimator.REVERSE);
-        animator.setRepeatCount(ValueAnimator.INFINITE);
-        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+        mAnimator = ValueAnimator.ofPropertyValuesHolder(pvhWidth, pvhHeight);
+        mAnimator.setRepeatMode(ValueAnimator.REVERSE);
+        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
+        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator valueAnimator) {
                 params.width = (Integer)valueAnimator.getAnimatedValue("width");
@@ -54,7 +56,15 @@
                 child.requestLayout();
             }
         });
-        animator.start();
+        mAnimator.start();
         setContentView(child);
     }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+    }
 }
diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp
index 2cebfe9..aca25eb 100644
--- a/tools/systemfeatures/Android.bp
+++ b/tools/systemfeatures/Android.bp
@@ -30,9 +30,9 @@
 genrule {
     name: "systemfeatures-gen-tests-srcs",
     cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " +
-        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true > $(location RoNoFeatures.java) && " +
+        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true --feature-apis=WATCH > $(location RoNoFeatures.java) && " +
         "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RwFeatures.java) && " +
-        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RoFeatures.java)",
+        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: --feature-apis=WATCH,PC > $(location RoFeatures.java)",
     out: [
         "RwNoFeatures.java",
         "RoNoFeatures.java",
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
index 9bfda45..e537ffc 100644
--- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
@@ -32,6 +32,7 @@
  * <pre>
  *   <cmd> com.foo.RoSystemFeatures --readonly=true \
  *           --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348
+ *           --feature-apis=WATCH,PC,LEANBACK
  * </pre>
  *
  * This generates a class that has the following signature:
@@ -45,12 +46,15 @@
  *     public static boolean hasFeatureAutomotive(Context context);
  *     @AssumeTrueForR8
  *     public static boolean hasFeatureVulkan(Context context);
+ *     public static boolean hasFeaturePc(Context context);
+ *     public static boolean hasFeatureLeanback(Context context);
  *     public static Boolean maybeHasFeature(String feature, int version);
  * }
  * </pre>
  */
 object SystemFeaturesGenerator {
     private const val FEATURE_ARG = "--feature="
+    private const val FEATURE_APIS_ARG = "--feature-apis="
     private const val READONLY_ARG = "--readonly="
     private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager")
     private val CONTEXT_CLASS = ClassName.get("android.content", "Context")
@@ -64,6 +68,15 @@
         println(" Options:")
         println("  --readonly=true|false    Whether to encode features as build-time constants")
         println("  --feature=\$NAME:\$VER   A feature+version pair (blank version == disabled)")
+        println("                           This will always generate associated query APIs,")
+        println("                           adding to or replacing those from `--feature-apis=`.")
+        println("  --feature-apis=\$NAME_1,\$NAME_2")
+        println("                           A comma-separated set of features for which to always")
+        println("                           generate named query APIs. If a feature in this set is")
+        println("                           not explicitly defined via `--feature=`, then a simple")
+        println("                           runtime passthrough API will be generated, regardless")
+        println("                           of the `--readonly` flag. This allows decoupling the")
+        println("                           API surface from variations in device feature sets.")
     }
 
     /** Main entrypoint for build-time system feature codegen. */
@@ -76,18 +89,42 @@
 
         var readonly = false
         var outputClassName: ClassName? = null
-        val features = mutableListOf<FeatureInfo>()
+        val featureArgs = mutableListOf<FeatureArg>()
+        // We could just as easily hardcode this list, as the static API surface should change
+        // somewhat infrequently, but this decouples the codegen from the framework completely.
+        val featureApiArgs = mutableSetOf<String>()
         for (arg in args) {
             when {
                 arg.startsWith(READONLY_ARG) ->
                     readonly = arg.substring(READONLY_ARG.length).toBoolean()
                 arg.startsWith(FEATURE_ARG) -> {
-                    features.add(parseFeatureArg(arg))
+                    featureArgs.add(parseFeatureArg(arg))
+                }
+                arg.startsWith(FEATURE_APIS_ARG) -> {
+                    featureApiArgs.addAll(
+                        arg.substring(FEATURE_APIS_ARG.length).split(",").map {
+                            parseFeatureName(it)
+                        }
+                    )
                 }
                 else -> outputClassName = ClassName.bestGuess(arg)
             }
         }
 
+        // First load in all of the feature APIs we want to generate. Explicit feature definitions
+        // will then override this set with the appropriate readonly and version value.
+        val features = mutableMapOf<String, FeatureInfo>()
+        featureApiArgs.associateByTo(
+            features,
+            { it },
+            { FeatureInfo(it, version = null, readonly = false) },
+        )
+        featureArgs.associateByTo(
+            features,
+            { it.name },
+            { FeatureInfo(it.name, it.version, readonly) },
+        )
+
         outputClassName
             ?: run {
                 println("Output class name must be provided.")
@@ -100,8 +137,8 @@
                 .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                 .addJavadoc("@hide")
 
-        addFeatureMethodsToClass(classBuilder, readonly, features)
-        addMaybeFeatureMethodToClass(classBuilder, readonly, features)
+        addFeatureMethodsToClass(classBuilder, features.values)
+        addMaybeFeatureMethodToClass(classBuilder, features.values)
 
         // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency.
         JavaFile.builder(outputClassName.packageName(), classBuilder.build())
@@ -115,13 +152,16 @@
      *   * "--feature=WATCH:7" -> Feature enabled w/ version 7
      *   * "--feature=WATCH:"  -> Feature disabled
      */
-    private fun parseFeatureArg(arg: String): FeatureInfo {
+    private fun parseFeatureArg(arg: String): FeatureArg {
         val featureArgs = arg.substring(FEATURE_ARG.length).split(":")
-        val name = featureArgs[0].let { if (!it.startsWith("FEATURE_")) "FEATURE_$it" else it }
+        val name = parseFeatureName(featureArgs[0])
         val version = featureArgs.getOrNull(1)?.toIntOrNull()
-        return FeatureInfo(name, version)
+        return FeatureArg(name, version)
     }
 
+    private fun parseFeatureName(name: String): String =
+        if (name.startsWith("FEATURE_")) name else "FEATURE_$name"
+
     /*
      * Adds per-feature query methods to the class with the form:
      * {@code public static boolean hasFeatureX(Context context)},
@@ -129,8 +169,7 @@
      */
     private fun addFeatureMethodsToClass(
         builder: TypeSpec.Builder,
-        readonly: Boolean,
-        features: List<FeatureInfo>
+        features: Collection<FeatureInfo>,
     ) {
         for (feature in features) {
             // Turn "FEATURE_FOO" into "hasFeatureFoo".
@@ -142,7 +181,7 @@
                     .returns(Boolean::class.java)
                     .addParameter(CONTEXT_CLASS, "context")
 
-            if (readonly) {
+            if (feature.readonly) {
                 val featureEnabled = compareValues(feature.version, 0) >= 0
                 methodBuilder.addAnnotation(
                     if (featureEnabled) ASSUME_TRUE_CLASS else ASSUME_FALSE_CLASS
@@ -158,19 +197,17 @@
             builder.addMethod(methodBuilder.build())
         }
 
-        if (!readonly) {
-            builder.addMethod(
-                MethodSpec.methodBuilder("hasFeatureFallback")
-                    .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
-                    .returns(Boolean::class.java)
-                    .addParameter(CONTEXT_CLASS, "context")
-                    .addParameter(String::class.java, "featureName")
-                    .addStatement(
-                        "return context.getPackageManager().hasSystemFeature(featureName, 0)"
-                    )
-                    .build()
-            )
-        }
+        // This is a trivial method, even if unused based on readonly-codegen, it does little harm
+        // to always include it.
+        builder.addMethod(
+            MethodSpec.methodBuilder("hasFeatureFallback")
+                .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
+                .returns(Boolean::class.java)
+                .addParameter(CONTEXT_CLASS, "context")
+                .addParameter(String::class.java, "featureName")
+                .addStatement("return context.getPackageManager().hasSystemFeature(featureName, 0)")
+                .build()
+        )
     }
 
     /*
@@ -185,8 +222,7 @@
      */
     private fun addMaybeFeatureMethodToClass(
         builder: TypeSpec.Builder,
-        readonly: Boolean,
-        features: List<FeatureInfo>
+        features: Collection<FeatureInfo>,
     ) {
         val methodBuilder =
             MethodSpec.methodBuilder("maybeHasFeature")
@@ -196,16 +232,27 @@
                 .addParameter(String::class.java, "featureName")
                 .addParameter(Int::class.java, "version")
 
-        if (readonly) {
-            methodBuilder.beginControlFlow("switch (featureName)")
-            for (feature in features) {
-                methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name)
-                if (feature.version != null) {
-                    methodBuilder.addStatement("return \$L >= version", feature.version)
-                } else {
-                    methodBuilder.addStatement("return false")
-                }
+        var hasSwitchBlock = false
+        for (feature in features) {
+            // We only return non-null results for queries against readonly-defined features.
+            if (!feature.readonly) {
+                continue
             }
+            if (!hasSwitchBlock) {
+                // As an optimization, only create the switch block if needed. Even an empty
+                // switch-on-string block can induce a hash, which we can avoid if readonly
+                // support is completely disabled.
+                hasSwitchBlock = true
+                methodBuilder.beginControlFlow("switch (featureName)")
+            }
+            methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name)
+            if (feature.version != null) {
+                methodBuilder.addStatement("return \$L >= version", feature.version)
+            } else {
+                methodBuilder.addStatement("return false")
+            }
+        }
+        if (hasSwitchBlock) {
             methodBuilder.addCode("default: ")
             methodBuilder.addStatement("break")
             methodBuilder.endControlFlow()
@@ -214,5 +261,7 @@
         builder.addMethod(methodBuilder.build())
     }
 
-    private data class FeatureInfo(val name: String, val version: Int?)
+    private data class FeatureArg(val name: String, val version: Int?)
+
+    private data class FeatureInfo(val name: String, val version: Int?, val readonly: Boolean)
 }
diff --git a/tools/systemfeatures/tests/PackageManager.java b/tools/systemfeatures/tests/PackageManager.java
index 645d500..db67048 100644
--- a/tools/systemfeatures/tests/PackageManager.java
+++ b/tools/systemfeatures/tests/PackageManager.java
@@ -19,6 +19,7 @@
 /** Stub for testing */
 public class PackageManager {
     public static final String FEATURE_AUTO = "automotive";
+    public static final String FEATURE_PC = "pc";
     public static final String FEATURE_VULKAN = "vulkan";
     public static final String FEATURE_WATCH = "watch";
     public static final String FEATURE_WIFI = "wifi";
diff --git a/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java
index 547d2cb..6dfd244 100644
--- a/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java
+++ b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java
@@ -68,6 +68,13 @@
         assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
         assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
         assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
+
+        // Also ensure we fall back to the PackageManager for feature APIs without an accompanying
+        // versioned feature definition.
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true);
+        assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue();
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false);
+        assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse();
     }
 
     @Test
@@ -127,6 +134,16 @@
         assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isFalse();
         assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 100)).isFalse();
 
+        // For feature APIs without an associated feature definition, conditional queries should
+        // report null, and explicit queries should report runtime-defined versions.
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(true);
+        assertThat(RoFeatures.hasFeaturePc(mContext)).isTrue();
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(false);
+        assertThat(RoFeatures.hasFeaturePc(mContext)).isFalse();
+        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, -1)).isNull();
+        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, 0)).isNull();
+        assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, 100)).isNull();
+
         // For undefined types, conditional queries should report null (unknown).
         assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", -1)).isNull();
         assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();