Merge "Move TracingTests to presubmit" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index cc0354c..569776e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -13020,6 +13020,8 @@
     method public void onSuggestedReplySent(@NonNull String, @NonNull CharSequence, int);
     method public final void unsnoozeNotification(@NonNull String);
     field public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
+    field @FlaggedApi("android.service.notification.notification_classification") public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
+    field @FlaggedApi("android.service.notification.notification_classification") public static final String EXTRA_NOTIFICATION_KEY = "android.service.notification.extra.NOTIFICATION_KEY";
     field public static final String FEEDBACK_RATING = "feedback.rating";
     field public static final String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService";
     field public static final int SOURCE_FROM_APP = 0; // 0x0
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index fad289c..edcdb6c 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -1047,7 +1047,7 @@
     private static String startComponentToString(@StartComponent int startComponent) {
         return switch (startComponent) {
             case START_COMPONENT_ACTIVITY -> "ACTIVITY";
-            case START_COMPONENT_BROADCAST -> "SERVICE";
+            case START_COMPONENT_BROADCAST -> "BROADCAST";
             case START_COMPONENT_CONTENT_PROVIDER -> "CONTENT PROVIDER";
             case START_COMPONENT_SERVICE -> "SERVICE";
             case START_COMPONENT_OTHER -> "OTHER";
diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl
index 8d25cad..4598421 100644
--- a/core/java/android/app/supervision/ISupervisionManager.aidl
+++ b/core/java/android/app/supervision/ISupervisionManager.aidl
@@ -21,5 +21,5 @@
  * {@hide}
  */
 interface ISupervisionManager {
-    boolean isSupervisionEnabled();
+    boolean isSupervisionEnabledForUser(int userId);
 }
diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java
index 8611a92..aee1cd9 100644
--- a/core/java/android/app/supervision/SupervisionManager.java
+++ b/core/java/android/app/supervision/SupervisionManager.java
@@ -17,6 +17,7 @@
 package android.app.supervision;
 
 import android.annotation.SystemService;
+import android.annotation.UserHandleAware;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.os.RemoteException;
@@ -45,13 +46,12 @@
      *
      * @hide
      */
+    @UserHandleAware
     public boolean isSupervisionEnabled() {
         try {
-            return mService.isSupervisionEnabled();
+            return mService.isSupervisionEnabledForUser(mContext.getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
-
-
 }
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 91cdf8d..1c9be6f 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -76,11 +76,15 @@
  * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION} before querying the service. If the feature is
  * absent, {@link Context#getSystemService} may return null.
  */
-@SystemService(Context.VCN_MANAGEMENT_SERVICE)
+@SystemService(VcnManager.VCN_MANAGEMENT_SERVICE_STRING)
 @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
 public class VcnManager {
     @NonNull private static final String TAG = VcnManager.class.getSimpleName();
 
+    // TODO: b/366598445: Expose and use Context.VCN_MANAGEMENT_SERVICE
+    /** @hide */
+    public static final String VCN_MANAGEMENT_SERVICE_STRING = "vcn_management";
+
     /**
      * Key for WiFi entry RSSI thresholds
      *
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 1fef602..a698b9d 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -788,6 +788,12 @@
     /** Parses an XML representation of BatteryUsageStats */
     public static BatteryUsageStats createFromXml(TypedXmlPullParser parser)
             throws XmlPullParserException, IOException {
+        return createBuilderFromXml(parser).build();
+    }
+
+    /** Parses an XML representation of BatteryUsageStats */
+    public static BatteryUsageStats.Builder createBuilderFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
         Builder builder = null;
         int eventType = parser.getEventType();
         while (eventType != XmlPullParser.END_DOCUMENT) {
@@ -862,7 +868,7 @@
             eventType = parser.next();
         }
 
-        return builder.build();
+        return builder;
     }
 
     @Override
@@ -978,10 +984,21 @@
          */
         @NonNull
         public BatteryUsageStats build() {
+            if (mBatteryConsumersCursorWindow == null) {
+                throw new IllegalStateException("Builder has been discarded");
+            }
             return new BatteryUsageStats(this);
         }
 
         /**
+         * Close this builder without actually calling ".build()". Do not attempt
+         * to continue using the builder after this call.
+         */
+        public void discard() {
+            mBatteryConsumersCursorWindow.close();
+        }
+
+        /**
          * Sets the battery capacity in milli-amp-hours.
          */
         public Builder setBatteryCapacity(double batteryCapacityMah) {
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index a12606b..b533225 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -77,6 +77,8 @@
 
     public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE = 0x0040;
 
+    public static final int FLAG_BATTERY_USAGE_STATS_ACCUMULATED = 0x0080;
+
     private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000;
 
     private final int mFlags;
@@ -328,6 +330,15 @@
         }
 
         /**
+         * Requests the full continuously accumulated battery usage stats: across reboots
+         * and most battery stats resets.
+         */
+        public Builder accumulated() {
+            mFlags |= FLAG_BATTERY_USAGE_STATS_ACCUMULATED;
+            return this;
+        }
+
+        /**
          * Requests to aggregate stored snapshots between the two supplied timestamps
          * @param fromTimestamp Exclusive starting timestamp, as per System.currentTimeMillis()
          * @param toTimestamp Inclusive ending timestamp, as per System.currentTimeMillis()
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index cfbf528..a5697fb 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -16,6 +16,8 @@
 
 package android.os;
 
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -138,14 +140,14 @@
             Log.w(TAG, "Failed to vibrate; no vibrator manager service.");
             return;
         }
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason=" + reason);
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "vibrate");
         try {
             mService.vibrate(uid, mContext.getDeviceId(), opPkg, effect, attributes, reason,
                     mToken);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to vibrate.", e);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -155,14 +157,14 @@
             Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager service.");
             return;
         }
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback, reason=" + reason);
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedback");
         try {
             mService.performHapticFeedback(mUid, mContext.getDeviceId(), mPackageName, constant,
                     reason, flags, privFlags);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to perform haptic feedback.", e);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -174,15 +176,14 @@
                             + " no vibrator manager service.");
             return;
         }
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR,
-                "performHapticFeedbackForInputDevice, reason=" + reason);
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
         try {
             mService.performHapticFeedbackForInputDevice(mUid, mContext.getDeviceId(), mPackageName,
                     constant, inputDeviceId, inputSource, reason, flags, privFlags);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to perform haptic feedback for input device.", e);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS
index c38ee08..325d274 100644
--- a/core/java/android/security/OWNERS
+++ b/core/java/android/security/OWNERS
@@ -10,3 +10,4 @@
 per-file FileIntegrityManager.java = file:platform/system/security:/fsverity/OWNERS
 per-file IFileIntegrityService.aidl = file:platform/system/security:/fsverity/OWNERS
 per-file *.aconfig = victorhsieh@google.com,eranm@google.com
+per-file *responsible_apis_flags.aconfig = haok@google.com
\ No newline at end of file
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index 88da8eb..48d7cf7 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -18,6 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -90,10 +91,11 @@
             = "android.service.notification.NotificationAssistantService";
 
     /**
-     * Activity Action: Show notification assistant detail setting page in NAS app.
+     * Activity Action: Show notification assistant detail setting page in the NAS app.
      * <p>
-     * In some cases, a matching Activity may not exist, so ensure you
-     * safeguard against this.
+     * To be implemented by the NAS to offer users additional customization of intelligence
+     * features. If the action is not implemented, the OS will not provide a link to it in the
+     * Settings UI.
      * <p>
      * Input: Nothing.
      * <p>
@@ -103,6 +105,30 @@
     public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS =
             "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
 
+    /**
+     * Activity Action: Open notification assistant feedback page in the NAS app.
+     * <p>
+     * If the NAS does not implement this page, the OS will not show any feedback calls to action in
+     * the UI.
+     * <p>
+     * Input: {@link #EXTRA_NOTIFICATION_KEY}, the {@link StatusBarNotification#getKey()} of the
+     * notification the user wants to file feedback for.
+     * <p>
+     * Output: Nothing.
+     */
+    @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS =
+            "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
+
+    /**
+     * A string extra containing the key of the notification that the user triggered feedback for.
+     *
+     * Extra for {@link #ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS}.
+     */
+    @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+    public static final String EXTRA_NOTIFICATION_KEY
+            = "android.service.notification.extra.NOTIFICATION_KEY";
 
     /**
      * Data type: int, the feedback rating score provided by user. The score can be any integer
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index cc5e583..fbc30ed 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -287,6 +287,13 @@
 }
 
 flag {
+    name: "enable_fully_immersive_in_desktop"
+    namespace: "lse_desktop_experience"
+    description: "Enabled the fully immersive experience from desktop"
+    bug: "359523924"
+}
+
+flag {
     name: "enable_display_focus_in_shell_transitions"
     namespace: "lse_desktop_experience"
     description: "Creates a shell transition when display focus switches."
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.java b/core/java/com/android/internal/util/ScreenshotRequest.java
index c8b7def..702e5e2 100644
--- a/core/java/com/android/internal/util/ScreenshotRequest.java
+++ b/core/java/com/android/internal/util/ScreenshotRequest.java
@@ -33,6 +33,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
+import android.view.Display;
 import android.view.WindowManager;
 
 import java.util.Objects;
@@ -53,11 +54,12 @@
     private final Bitmap mBitmap;
     private final Rect mBoundsInScreen;
     private final Insets mInsets;
+    private final int mDisplayId;
 
     private ScreenshotRequest(
             @WindowManager.ScreenshotType int type, @WindowManager.ScreenshotSource int source,
             ComponentName topComponent, int taskId, int userId,
-            Bitmap bitmap, Rect boundsInScreen, Insets insets) {
+            Bitmap bitmap, Rect boundsInScreen, Insets insets, int displayId) {
         mType = type;
         mSource = source;
         mTopComponent = topComponent;
@@ -66,6 +68,7 @@
         mBitmap = bitmap;
         mBoundsInScreen = boundsInScreen;
         mInsets = insets;
+        mDisplayId = displayId;
     }
 
     ScreenshotRequest(Parcel in) {
@@ -77,6 +80,7 @@
         mBitmap = HardwareBitmapBundler.bundleToHardwareBitmap(in.readTypedObject(Bundle.CREATOR));
         mBoundsInScreen = in.readTypedObject(Rect.CREATOR);
         mInsets = in.readTypedObject(Insets.CREATOR);
+        mDisplayId = in.readInt();
     }
 
     @WindowManager.ScreenshotType
@@ -113,6 +117,10 @@
         return mTopComponent;
     }
 
+    public int getDisplayId() {
+        return mDisplayId;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -128,6 +136,7 @@
         dest.writeTypedObject(HardwareBitmapBundler.hardwareBitmapToBundle(mBitmap), 0);
         dest.writeTypedObject(mBoundsInScreen, 0);
         dest.writeTypedObject(mInsets, 0);
+        dest.writeInt(mDisplayId);
     }
 
     @NonNull
@@ -161,6 +170,7 @@
         private int mTaskId = INVALID_TASK_ID;
         private int mUserId = USER_NULL;
         private ComponentName mTopComponent;
+        private int mDisplayId = Display.INVALID_DISPLAY;
 
         /**
          * Begin building a ScreenshotRequest.
@@ -193,7 +203,7 @@
             }
 
             return new ScreenshotRequest(mType, mSource, mTopComponent, mTaskId, mUserId, mBitmap,
-                    mBoundsInScreen, mInsets);
+                    mBoundsInScreen, mInsets, mDisplayId);
         }
 
         /**
@@ -255,6 +265,16 @@
             mInsets = insets;
             return this;
         }
+
+        /**
+         * Set the display ID for this request.
+         *
+         * @param displayId see {@link Display}
+         */
+        public Builder setDisplayId(int displayId) {
+            mDisplayId = displayId;
+            return this;
+        }
     }
 
     /**
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
index 89acbc7..0ce403e 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
@@ -35,6 +35,7 @@
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
 import android.os.Parcel;
+import android.view.Display;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -64,10 +65,12 @@
         assertNull("Bitmap was expected to be null", out.getBitmap());
         assertNull("Bounds were expected to be null", out.getBoundsInScreen());
         assertEquals(Insets.NONE, out.getInsets());
+        assertEquals(Display.INVALID_DISPLAY, out.getDisplayId());
     }
 
     @Test
     public void testProvidedScreenshot() {
+        int displayId = 5;
         Bitmap bitmap = makeHardwareBitmap(50, 50);
         ScreenshotRequest in =
                 new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
@@ -77,6 +80,7 @@
                         .setBitmap(bitmap)
                         .setBoundsOnScreen(new Rect(10, 10, 60, 60))
                         .setInsets(Insets.of(2, 3, 4, 5))
+                        .setDisplayId(displayId)
                         .build();
 
         Parcel parcel = Parcel.obtain();
@@ -92,6 +96,7 @@
         assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
         assertEquals(new Rect(10, 10, 60, 60), out.getBoundsInScreen());
         assertEquals(Insets.of(2, 3, 4, 5), out.getInsets());
+        assertEquals(displayId, out.getDisplayId());
     }
 
     @Test
diff --git a/libs/appfunctions/tests/Android.bp b/libs/appfunctions/tests/Android.bp
new file mode 100644
index 0000000..6f5eff3
--- /dev/null
+++ b/libs/appfunctions/tests/Android.bp
@@ -0,0 +1,41 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AppFunctionsSidecarTestCases",
+    team: "trendy_team_system_intelligence",
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "androidx.core_core-ktx",
+        "com.google.android.appfunctions.sidecar.impl",
+        "junit",
+        "kotlin-test",
+        "mockito-target-extended-minus-junit4",
+        "platform-test-annotations",
+        "testables",
+        "testng",
+        "truth",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+}
diff --git a/libs/appfunctions/tests/AndroidManifest.xml b/libs/appfunctions/tests/AndroidManifest.xml
new file mode 100644
index 0000000..9a7d460
--- /dev/null
+++ b/libs/appfunctions/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.appfunctions.sidecar.tests">
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.mock" />
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.google.android.appfunctions.sidecar.tests">
+    </instrumentation>
+</manifest>
diff --git a/libs/appfunctions/tests/AndroidTest.xml b/libs/appfunctions/tests/AndroidTest.xml
new file mode 100644
index 0000000..8251212
--- /dev/null
+++ b/libs/appfunctions/tests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for AppFunctions Sidecar Tests">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="AppFunctionsSidecarTestCases.apk" />
+    </target_preparer>
+    <option name="test-tag" value="AppFunctionsSidecarTestCases" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.google.android.appfunctions.sidecar.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
new file mode 100644
index 0000000..1f9fddd
--- /dev/null
+++ b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.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.google.android.appfunctions.sidecar.tests
+
+import android.app.appfunctions.ExecuteAppFunctionRequest
+import android.app.appfunctions.ExecuteAppFunctionResponse
+import android.app.appsearch.GenericDocument
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.appfunctions.sidecar.SidecarConverter
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SidecarConverterTest {
+    @Test
+    fun getSidecarExecuteAppFunctionRequest_sameContents() {
+        val extras = Bundle()
+        extras.putString("extra", "value")
+        val parameters: GenericDocument =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("testLong", 23)
+                .build()
+        val platformRequest: ExecuteAppFunctionRequest =
+            ExecuteAppFunctionRequest.Builder("targetPkg", "targetFunctionId")
+                .setExtras(extras)
+                .setParameters(parameters)
+                .build()
+
+        val sidecarRequest = SidecarConverter.getSidecarExecuteAppFunctionRequest(platformRequest)
+
+        assertThat(sidecarRequest.targetPackageName).isEqualTo("targetPkg")
+        assertThat(sidecarRequest.functionIdentifier).isEqualTo("targetFunctionId")
+        assertThat(sidecarRequest.parameters).isEqualTo(parameters)
+        assertThat(sidecarRequest.extras.size()).isEqualTo(1)
+        assertThat(sidecarRequest.extras.getString("extra")).isEqualTo("value")
+    }
+
+    @Test
+    fun getPlatformExecuteAppFunctionRequest_sameContents() {
+        val extras = Bundle()
+        extras.putString("extra", "value")
+        val parameters: GenericDocument =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyLong("testLong", 23)
+                .build()
+        val sidecarRequest =
+            com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder(
+                "targetPkg",
+                "targetFunctionId"
+            )
+                .setExtras(extras)
+                .setParameters(parameters)
+                .build()
+
+        val platformRequest = SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest)
+
+        assertThat(platformRequest.targetPackageName).isEqualTo("targetPkg")
+        assertThat(platformRequest.functionIdentifier).isEqualTo("targetFunctionId")
+        assertThat(platformRequest.parameters).isEqualTo(parameters)
+        assertThat(platformRequest.extras.size()).isEqualTo(1)
+        assertThat(platformRequest.extras.getString("extra")).isEqualTo("value")
+    }
+
+    @Test
+    fun getSidecarExecuteAppFunctionResponse_successResponse_sameContents() {
+        val resultGd: GenericDocument =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
+                .build()
+        val platformResponse = ExecuteAppFunctionResponse.newSuccess(resultGd, null)
+
+        val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
+            platformResponse
+        )
+
+        assertThat(sidecarResponse.isSuccess).isTrue()
+        assertThat(
+            sidecarResponse.resultDocument.getProperty(
+                ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
+            )
+        )
+            .isEqualTo(booleanArrayOf(true))
+        assertThat(sidecarResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
+        assertThat(sidecarResponse.errorMessage).isNull()
+    }
+
+    @Test
+    fun getSidecarExecuteAppFunctionResponse_errorResponse_sameContents() {
+        val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
+        val platformResponse =
+            ExecuteAppFunctionResponse.newFailure(
+                ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                null,
+                null
+            )
+
+        val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
+            platformResponse
+        )
+
+        assertThat(sidecarResponse.isSuccess).isFalse()
+        assertThat(sidecarResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
+        assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id)
+        assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
+        assertThat(sidecarResponse.resultCode)
+            .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR)
+        assertThat(sidecarResponse.errorMessage).isNull()
+    }
+
+    @Test
+    fun getPlatformExecuteAppFunctionResponse_successResponse_sameContents() {
+        val resultGd: GenericDocument =
+            GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+                .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
+                .build()
+        val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse
+            .newSuccess(resultGd, null)
+
+        val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
+            sidecarResponse
+        )
+
+        assertThat(platformResponse.isSuccess).isTrue()
+        assertThat(
+            platformResponse.resultDocument.getProperty(
+                ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
+            )
+        )
+            .isEqualTo(booleanArrayOf(true))
+        assertThat(platformResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
+        assertThat(platformResponse.errorMessage).isNull()
+    }
+
+    @Test
+    fun getPlatformExecuteAppFunctionResponse_errorResponse_sameContents() {
+        val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
+        val sidecarResponse =
+            com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure(
+                ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+                null,
+                null
+            )
+
+        val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
+            sidecarResponse
+        )
+
+        assertThat(platformResponse.isSuccess).isFalse()
+        assertThat(platformResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
+        assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id)
+        assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
+        assertThat(platformResponse.resultCode)
+            .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR)
+        assertThat(platformResponse.errorMessage).isNull()
+    }
+}
diff --git a/packages/SettingsLib/ActionButtonsPreference/Android.bp b/packages/SettingsLib/ActionButtonsPreference/Android.bp
index 71ecb4c..37a0e79 100644
--- a/packages/SettingsLib/ActionButtonsPreference/Android.bp
+++ b/packages/SettingsLib/ActionButtonsPreference/Android.bp
@@ -19,6 +19,7 @@
 
     static_libs: [
         "androidx.preference_preference",
+        "SettingsLibSettingsTheme",
     ],
 
     sdk_version: "system_current",
diff --git a/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
new file mode 100644
index 0000000..fc63c0f
--- /dev/null
+++ b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_margin="@dimen/settingslib_expressive_space_extrasmall4"
+    android:paddingHorizontal="@dimen/settingslib_expressive_space_extrasmall4"
+    android:orientation="horizontal">
+
+    <LinearLayout
+        android:id="@+id/action1"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button1"
+            style="@style/SettingsLibActionButton.Expressive"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_medium5"
+            android:layout_gravity="center_horizontal" />
+        <TextView
+            android:id="@+id/text1"
+            style="@style/SettingsLibActionButton.Expressive.Label"
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/action2"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button2"
+            style="@style/SettingsLibActionButton.Expressive"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_medium5"
+            android:layout_gravity="center_horizontal" />
+        <TextView
+            android:id="@+id/text2"
+            style="@style/SettingsLibActionButton.Expressive.Label"
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/action3"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button3"
+            style="@style/SettingsLibActionButton.Expressive"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_medium5"
+            android:layout_gravity="center_horizontal" />
+        <TextView
+            android:id="@+id/text3"
+            style="@style/SettingsLibActionButton.Expressive.Label"
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/action4"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button4"
+            style="@style/SettingsLibActionButton.Expressive"
+            android:layout_width="@dimen/settingslib_expressive_space_large3"
+            android:layout_height="@dimen/settingslib_expressive_space_medium5"
+            android:layout_gravity="center_horizontal" />
+        <TextView
+            android:id="@+id/text4"
+            style="@style/SettingsLibActionButton.Expressive.Label"
+            android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+    </LinearLayout>
+</LinearLayout>
diff --git a/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml b/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml
new file mode 100644
index 0000000..cc948a6
--- /dev/null
+++ b/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <style name="SettingsLibActionButton.Expressive" parent="SettingsLibButtonStyle.Expressive.Tonal">
+        <item name="android:backgroundTint">@color/settingslib_materialColorPrimaryContainer</item>
+        <item name="iconTint">@color/settingslib_materialColorOnPrimaryContainer</item>
+        <item name="iconGravity">textTop</item>
+    </style>
+
+    <style name="SettingsLibActionButton.Expressive.Label" parent="SettingsLibTextAppearance.Emphasized.Title.Small">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:minWidth">@dimen/settingslib_expressive_space_small3</item>
+        <item name="android:minHeight">@dimen/settingslib_expressive_space_small3</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnSurface</item>
+        <item name="android:layout_gravity">center</item>
+    </style>
+
+</resources>
diff --git a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
index 5dc11cf..f011039 100644
--- a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
+++ b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
@@ -26,6 +26,8 @@
 import android.util.Log;
 import android.view.View;
 import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.StringRes;
@@ -34,6 +36,8 @@
 
 import com.android.settingslib.widget.preference.actionbuttons.R;
 
+import com.google.android.material.button.MaterialButton;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -98,7 +102,10 @@
     }
 
     private void init() {
-        setLayoutResource(R.layout.settingslib_action_buttons);
+        int resId = SettingsThemeHelper.isExpressiveTheme(getContext())
+                ? R.layout.settingslib_expressive_action_buttons
+                : R.layout.settingslib_action_buttons;
+        setLayoutResource(resId);
         setSelectable(false);
 
         final Resources res = getContext().getResources();
@@ -127,6 +134,21 @@
         mButton3Info.mButton = (Button) holder.findViewById(R.id.button3);
         mButton4Info.mButton = (Button) holder.findViewById(R.id.button4);
 
+        if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+            mButton1Info.mIsExpressive = true;
+            mButton1Info.mTextView = (TextView) holder.findViewById(R.id.text1);
+            mButton1Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action1);
+            mButton2Info.mIsExpressive = true;
+            mButton2Info.mTextView = (TextView) holder.findViewById(R.id.text2);
+            mButton2Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action2);
+            mButton3Info.mIsExpressive = true;
+            mButton3Info.mTextView = (TextView) holder.findViewById(R.id.text3);
+            mButton3Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action3);
+            mButton4Info.mIsExpressive = true;
+            mButton4Info.mTextView = (TextView) holder.findViewById(R.id.text4);
+            mButton4Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action4);
+        }
+
         mDivider1 = holder.findViewById(R.id.divider1);
         mDivider2 = holder.findViewById(R.id.divider2);
         mDivider3 = holder.findViewById(R.id.divider3);
@@ -169,45 +191,47 @@
             mVisibleButtonInfos.add(mButton4Info);
         }
 
-        final boolean isRtl = getContext().getResources().getConfiguration()
-                .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
-        switch (mVisibleButtonInfos.size()) {
-            case SINGLE_BUTTON_STYLE :
-                if (isRtl) {
-                    setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
-                } else {
-                    setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
-                }
-                break;
-            case TWO_BUTTONS_STYLE :
-                if (isRtl) {
-                    setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
-                } else {
-                    setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
-                }
-                break;
-            case THREE_BUTTONS_STYLE :
-                if (isRtl) {
-                    setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
-                } else {
-                    setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
-                }
-                break;
-            case FOUR_BUTTONS_STYLE :
-                if (isRtl) {
-                    setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
-                } else {
-                    setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
-                }
-                break;
-            default:
-                Log.e(TAG, "No visible buttons info, skip background settings.");
-                break;
-        }
+        if (!SettingsThemeHelper.isExpressiveTheme(getContext())) {
+            final boolean isRtl = getContext().getResources().getConfiguration()
+                    .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+            switch (mVisibleButtonInfos.size()) {
+                case SINGLE_BUTTON_STYLE :
+                    if (isRtl) {
+                        setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
+                    } else {
+                        setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
+                    }
+                    break;
+                case TWO_BUTTONS_STYLE :
+                    if (isRtl) {
+                        setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
+                    } else {
+                        setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
+                    }
+                    break;
+                case THREE_BUTTONS_STYLE :
+                    if (isRtl) {
+                        setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
+                    } else {
+                        setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
+                    }
+                    break;
+                case FOUR_BUTTONS_STYLE :
+                    if (isRtl) {
+                        setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
+                    } else {
+                        setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
+                    }
+                    break;
+                default:
+                    Log.e(TAG, "No visible buttons info, skip background settings.");
+                    break;
+            }
 
-        setupDivider1();
-        setupDivider2();
-        setupDivider3();
+            setupDivider1();
+            setupDivider2();
+            setupDivider3();
+        }
     }
 
     private void setupBackgrounds(
@@ -509,23 +533,43 @@
 
     static class ButtonInfo {
         private Button mButton;
+        private TextView mTextView;
+        private LinearLayout mActionLayout;
         private CharSequence mText;
         private Drawable mIcon;
         private View.OnClickListener mListener;
         private boolean mIsEnabled = true;
         private boolean mIsVisible = true;
+        private boolean mIsExpressive = false;
 
         void setUpButton() {
-            mButton.setText(mText);
+            if (mIsExpressive) {
+                mTextView.setText(mText);
+                if (mButton instanceof MaterialButton) {
+                    ((MaterialButton) mButton).setIcon(mIcon);
+                }
+            } else {
+                mButton.setText(mText);
+                mButton.setCompoundDrawablesWithIntrinsicBounds(
+                        null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+            }
+
             mButton.setOnClickListener(mListener);
             mButton.setEnabled(mIsEnabled);
-            mButton.setCompoundDrawablesWithIntrinsicBounds(
-                    null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+
 
             if (shouldBeVisible()) {
                 mButton.setVisibility(View.VISIBLE);
+                if (mIsExpressive) {
+                    mTextView.setVisibility(View.VISIBLE);
+                    mActionLayout.setVisibility(View.VISIBLE);
+                }
             } else {
                 mButton.setVisibility(View.GONE);
+                if (mIsExpressive) {
+                    mTextView.setVisibility(View.GONE);
+                    mActionLayout.setVisibility(View.GONE);
+                }
             }
         }
 
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
index 2320aab..0542c51 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
@@ -48,6 +48,7 @@
     <dimen name="settingslib_expressive_space_medium2">36dp</dimen>
     <dimen name="settingslib_expressive_space_medium3">40dp</dimen>
     <dimen name="settingslib_expressive_space_medium4">48dp</dimen>
+    <dimen name="settingslib_expressive_space_medium5">56dp</dimen>
     <dimen name="settingslib_expressive_space_large1">60dp</dimen>
     <dimen name="settingslib_expressive_space_large2">64dp</dimen>
     <dimen name="settingslib_expressive_space_large3">72dp</dimen>
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
index 04ae80e..442def9 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
@@ -169,4 +169,23 @@
         <item name="android:focusable">false</item>
         <item name="thumbIcon">@drawable/settingslib_expressive_switch_thumb_icon</item>
     </style>
+
+    <style name="SettingsLibButtonStyle.Expressive.Tonal"
+        parent="@style/Widget.Material3.Button.TonalButton">
+        <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:gravity">center</item>
+        <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item>
+        <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item>
+        <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item>
+        <item name="android:backgroundTint">@color/settingslib_materialColorSecondaryContainer</item>
+        <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnSecondaryContainer</item>
+        <item name="android:textSize">14sp</item>
+        <item name="iconGravity">textStart</item>
+        <item name="iconTint">@color/settingslib_materialColorOnSecondaryContainer</item>
+        <item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
+    </style>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/animation/lib/Android.bp b/packages/SystemUI/animation/lib/Android.bp
index 4324d463..d9230ec 100644
--- a/packages/SystemUI/animation/lib/Android.bp
+++ b/packages/SystemUI/animation/lib/Android.bp
@@ -33,6 +33,20 @@
     ],
 }
 
+// This is the core animation library written in java and can be depended by java sdk libraries.
+// Please don't introduce kotlin code in this target since kotlin is incompatible with sdk
+// libraries.
+java_library {
+    name: "PlatformAnimationLib-core",
+    srcs: [
+        "src/com/android/systemui/animation/*.java",
+        ":PlatformAnimationLib-aidl",
+    ],
+    static_libs: [
+        "WindowManager-Shell-shared",
+    ],
+}
+
 filegroup {
     name: "PlatformAnimationLib-aidl",
     srcs: [
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
new file mode 100644
index 0000000..64bedd3
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.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.systemui.animation;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.app.ActivityOptions.LaunchCookie;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.RemoteException;
+import android.util.Log;
+import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
+
+import com.android.systemui.animation.shared.IOriginTransitions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * A session object that holds origin transition states for starting an activity from an on-screen
+ * UI component and smoothly transitioning back from the activity to the same UI component.
+ */
+public class OriginTransitionSession {
+    private static final String TAG = "OriginTransitionSession";
+    static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {NOT_STARTED, STARTED, CANCELLED})
+    private @interface State {}
+
+    @State private static final int NOT_STARTED = 0;
+    @State private static final int STARTED = 1;
+    @State private static final int CANCELLED = 5;
+
+    private final String mName;
+    @Nullable private final IOriginTransitions mOriginTransitions;
+    private final Predicate<RemoteTransition> mIntentStarter;
+    @Nullable private final IRemoteTransition mEntryTransition;
+    @Nullable private final IRemoteTransition mExitTransition;
+    private final AtomicInteger mState = new AtomicInteger(NOT_STARTED);
+
+    @Nullable private RemoteTransition mOriginTransition;
+
+    private OriginTransitionSession(
+            String name,
+            @Nullable IOriginTransitions originTransitions,
+            Predicate<RemoteTransition> intentStarter,
+            @Nullable IRemoteTransition entryTransition,
+            @Nullable IRemoteTransition exitTransition) {
+        mName = name;
+        mOriginTransitions = originTransitions;
+        mIntentStarter = intentStarter;
+        mEntryTransition = entryTransition;
+        mExitTransition = exitTransition;
+        if (hasExitTransition() && !hasEntryTransition()) {
+            throw new IllegalArgumentException(
+                    "Entry transition must be supplied if you want to play an exit transition!");
+        }
+    }
+
+    /**
+     * Launch the target intent with the supplied entry transition. After this method, the entry
+     * transition is expected to receive callbacks. The exit transition will be registered and
+     * triggered when the system detects a return from the launched activity to the launching
+     * activity.
+     */
+    public boolean start() {
+        if (!mState.compareAndSet(NOT_STARTED, STARTED)) {
+            logE("start: illegal state - " + stateToString(mState.get()));
+            return false;
+        }
+
+        RemoteTransition remoteTransition = null;
+        if (hasEntryTransition() && hasExitTransition()) {
+            logD("start: starting with entry and exit transition.");
+            try {
+                remoteTransition =
+                        mOriginTransition =
+                                mOriginTransitions.makeOriginTransition(
+                                        new RemoteTransition(mEntryTransition, mName + "-entry"),
+                                        new RemoteTransition(mExitTransition, mName + "-exit"));
+            } catch (RemoteException e) {
+                logE("Unable to create origin transition!", e);
+            }
+        } else if (hasEntryTransition()) {
+            logD("start: starting with entry transition.");
+            remoteTransition = new RemoteTransition(mEntryTransition, mName + "-entry");
+
+        } else {
+            logD("start: starting without transition.");
+        }
+        if (mIntentStarter.test(remoteTransition)) {
+            return true;
+        } else {
+            // Animation is cancelled by intent starter.
+            logD("start: cancelled by intent starter!");
+            cancel();
+            return false;
+        }
+    }
+
+    /**
+     * Cancel the current transition and the registered exit transition if it exists. After this
+     * method, this session object can no longer be used. Clients need to create a new session
+     * object if they want to launch another intent with origin transition.
+     */
+    public void cancel() {
+        final int lastState = mState.getAndSet(CANCELLED);
+        if (lastState == CANCELLED || lastState == NOT_STARTED) {
+            return;
+        }
+        logD("cancel: cancelled transition. Last state: " + stateToString(lastState));
+        if (mOriginTransition != null) {
+            try {
+                mOriginTransitions.cancelOriginTransition(mOriginTransition);
+                mOriginTransition = null;
+            } catch (RemoteException e) {
+                logE("Unable to cancel origin transition!", e);
+            }
+        }
+    }
+
+    private boolean hasEntryTransition() {
+        return mEntryTransition != null;
+    }
+
+    private boolean hasExitTransition() {
+        return mOriginTransitions != null && mExitTransition != null;
+    }
+
+    private void logD(String msg) {
+        if (DEBUG) {
+            Log.d(TAG, "Session[" + mName + "] - " + msg);
+        }
+    }
+
+    private void logE(String msg) {
+        Log.e(TAG, "Session[" + mName + "] - " + msg);
+    }
+
+    private void logE(String msg, Throwable e) {
+        Log.e(TAG, "Session[" + mName + "] - " + msg, e);
+    }
+
+    private static String stateToString(@State int state) {
+        switch (state) {
+            case NOT_STARTED:
+                return "NOT_STARTED";
+            case STARTED:
+                return "STARTED";
+            case CANCELLED:
+                return "CANCELLED";
+            default:
+                return "UNKNOWN(" + state + ")";
+        }
+    }
+
+    /** A builder to build a {@link OriginTransitionSession}. */
+    public static class Builder {
+        private final Context mContext;
+        @Nullable private final IOriginTransitions mOriginTransitions;
+        @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier;
+        @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier;
+        private String mName;
+        @Nullable private Predicate<RemoteTransition> mIntentStarter;
+
+        /** Create a builder that only supports entry transition. */
+        public Builder(Context context) {
+            this(context, /* originTransitions= */ null);
+        }
+
+        /** Create a builder that supports both entry and return transition. */
+        public Builder(Context context, @Nullable IOriginTransitions originTransitions) {
+            mContext = context;
+            mOriginTransitions = originTransitions;
+            mName = context.getPackageName();
+        }
+
+        /** Specify a name that is used in logging. */
+        public Builder withName(String name) {
+            mName = name;
+            return this;
+        }
+
+        /** Specify an intent that will be launched when the session started. */
+        public Builder withIntent(Intent intent) {
+            return withIntentStarter(
+                    transition -> {
+                        mContext.startActivity(
+                                intent, createDefaultActivityOptions(transition).toBundle());
+                        return true;
+                    });
+        }
+
+        /** Specify a pending intent that will be launched when the session started. */
+        public Builder withPendingIntent(PendingIntent pendingIntent) {
+            return withIntentStarter(
+                    transition -> {
+                        try {
+                            pendingIntent.send(createDefaultActivityOptions(transition).toBundle());
+                            return true;
+                        } catch (PendingIntent.CanceledException e) {
+                            Log.e(TAG, "Failed to launch pending intent!", e);
+                            return false;
+                        }
+                    });
+        }
+
+        private static ActivityOptions createDefaultActivityOptions(
+                @Nullable RemoteTransition transition) {
+            ActivityOptions options =
+                    transition == null
+                            ? ActivityOptions.makeBasic()
+                            : ActivityOptions.makeRemoteTransition(transition);
+            LaunchCookie cookie = new LaunchCookie();
+            options.setLaunchCookie(cookie);
+            return options;
+        }
+
+        /**
+         * Specify an intent starter function that will be called to start an activity. The function
+         * accepts an optional {@link RemoteTransition} object which can be used to create an {@link
+         * ActivityOptions} for the activity launch. The function can also return a {@code false}
+         * result to cancel the session.
+         *
+         * <p>Note: it's encouraged to use {@link #withIntent(Intent)} or {@link
+         * #withPendingIntent(PendingIntent)} instead of this method. Use it only if the default
+         * activity launch code doesn't satisfy your requirement.
+         */
+        public Builder withIntentStarter(Predicate<RemoteTransition> intentStarter) {
+            mIntentStarter = intentStarter;
+            return this;
+        }
+
+        /** Add an entry transition to the builder. */
+        public Builder withEntryTransition(IRemoteTransition transition) {
+            mEntryTransitionSupplier = () -> transition;
+            return this;
+        }
+
+        /** Add an exit transition to the builder. */
+        public Builder withExitTransition(IRemoteTransition transition) {
+            mExitTransitionSupplier = () -> transition;
+            return this;
+        }
+
+        /** Build an {@link OriginTransitionSession}. */
+        public OriginTransitionSession build() {
+            if (mIntentStarter == null) {
+                throw new IllegalArgumentException("No intent, pending intent, or intent starter!");
+            }
+            return new OriginTransitionSession(
+                    mName,
+                    mOriginTransitions,
+                    mIntentStarter,
+                    mEntryTransitionSupplier == null ? null : mEntryTransitionSupplier.get(),
+                    mExitTransitionSupplier == null ? null : mExitTransitionSupplier.get());
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/lib/tests/Android.bp b/packages/SystemUI/animation/lib/tests/Android.bp
new file mode 100644
index 0000000..c1a3e84
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/Android.bp
@@ -0,0 +1,47 @@
+// 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 {
+    default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_test {
+    name: "PlatformAnimationLibCoreTests",
+
+    defaults: [
+        "platform_app_defaults",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    dxflags: ["--multi-dex"],
+    platform_apis: true,
+    test_suites: ["device-tests"],
+    static_libs: [
+        "PlatformAnimationLib-core",
+        "platform-test-rules",
+        "testables",
+    ],
+    compile_multilib: "both",
+    libs: [
+        "android.test.runner.stubs.system",
+        "android.test.base.stubs.system",
+    ],
+
+    certificate: "platform",
+
+    manifest: "AndroidManifest.xml",
+}
diff --git a/packages/SystemUI/animation/lib/tests/AndroidManifest.xml b/packages/SystemUI/animation/lib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..1788abf
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:sharedUserId="android.uid.system"
+    package="com.android.systemui.animation.core.tests" >
+
+     <application android:debuggable="true" android:testOnly="true">
+         <uses-library android:name="android.test.runner" />
+     </application>
+
+    <instrumentation android:name="android.testing.TestableInstrumentation"
+        android:targetPackage="com.android.systemui.animation.core.tests"
+        android:label="Tests for PlatformAnimationLib-core" />
+</manifest>
diff --git a/packages/SystemUI/animation/lib/tests/AndroidTest.xml b/packages/SystemUI/animation/lib/tests/AndroidTest.xml
new file mode 100644
index 0000000..0f37d7a
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/AndroidTest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Runs Tests for PlatformAnimationLib-core.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="PlatformAnimationLibCoreTests.apk" />
+        <option name="install-arg" value="-t" />
+    </target_preparer>
+
+    <!-- Among other reasons, root access is needed for screen recording artifacts. -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+        <option name="force-root" value="true" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.systemui.animation.core.tests" />
+        <option name="runner" value="android.testing.TestableInstrumentation" />
+        <option name="test-filter-dir" value="/data/data/com.android.systemui.animation.core.tests" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/data/user/0/com.android.systemui.animation.core.tests/files" />
+        <option name="collect-on-run-ended-only" value="false" />
+    </metrics_collector>
+</configuration>
diff --git a/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java b/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java
new file mode 100644
index 0000000..287e53b
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java
@@ -0,0 +1,406 @@
+/*
+ * 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.animation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
+import android.window.WindowAnimationState;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.systemui.animation.shared.IOriginTransitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.function.Predicate;
+
+/** Unit tests for {@link OriginTransitionSession}. */
+@SmallTest
+@RunWith(JUnit4.class)
+public final class OriginTransitionSessionTest {
+    private static final ComponentName TEST_ACTIVITY_1 = new ComponentName("test", "Activity1");
+    private static final ComponentName TEST_ACTIVITY_2 = new ComponentName("test", "Activity2");
+    private static final ComponentName TEST_ACTIVITY_3 = new ComponentName("test", "Activity3");
+
+    private FakeIOriginTransitions mIOriginTransitions;
+    private Instrumentation mInstrumentation;
+    private FakeIntentStarter mIntentStarter;
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getTargetContext();
+        mIOriginTransitions = new FakeIOriginTransitions();
+        mIntentStarter = new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2);
+    }
+
+    @Test
+    public void sessionStart_withEntryAndExitTransition_transitionsPlayed() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isTrue();
+        assertThat(entry.started()).isTrue();
+
+        runReturnTransition(mIntentStarter);
+
+        assertThat(exit.started()).isTrue();
+    }
+
+    @Test
+    public void sessionStart_withEntryTransition_transitionPlayed() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .build();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isTrue();
+        assertThat(entry.started()).isTrue();
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void sessionStart_withoutTransition_launchedIntent() {
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .build();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isTrue();
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void sessionStart_cancelledByIntentStarter_transitionNotPlayed() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        mIntentStarter =
+                new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2, /* result= */ false);
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isFalse();
+        assertThat(entry.started()).isFalse();
+        assertThat(exit.started()).isFalse();
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void sessionStart_alreadyStarted_noOp() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+        session.start();
+        entry.reset();
+        mIntentStarter.reset();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isFalse();
+        assertThat(entry.started()).isFalse();
+    }
+
+    @Test
+    public void sessionStart_alreadyCancelled_noOp() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+        session.cancel();
+
+        session.start();
+
+        assertThat(mIntentStarter.hasLaunched()).isFalse();
+        assertThat(entry.started()).isFalse();
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void sessionCancelled_returnTransitionNotPlayed() {
+        FakeRemoteTransition entry = new FakeRemoteTransition();
+        FakeRemoteTransition exit = new FakeRemoteTransition();
+        OriginTransitionSession session =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(mIntentStarter)
+                        .withEntryTransition(entry)
+                        .withExitTransition(exit)
+                        .build();
+
+        session.start();
+        session.cancel();
+
+        assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+    }
+
+    @Test
+    public void multipleSessionsStarted_allTransitionsPlayed() {
+        FakeRemoteTransition entry1 = new FakeRemoteTransition();
+        FakeRemoteTransition exit1 = new FakeRemoteTransition();
+        FakeIntentStarter starter1 = mIntentStarter;
+        OriginTransitionSession session1 =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(starter1)
+                        .withEntryTransition(entry1)
+                        .withExitTransition(exit1)
+                        .build();
+        FakeRemoteTransition entry2 = new FakeRemoteTransition();
+        FakeRemoteTransition exit2 = new FakeRemoteTransition();
+        FakeIntentStarter starter2 = new FakeIntentStarter(TEST_ACTIVITY_2, TEST_ACTIVITY_3);
+        OriginTransitionSession session2 =
+                new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+                        .withIntentStarter(starter2)
+                        .withEntryTransition(entry2)
+                        .withExitTransition(exit2)
+                        .build();
+
+        session1.start();
+
+        assertThat(starter1.hasLaunched()).isTrue();
+        assertThat(entry1.started()).isTrue();
+
+        session2.start();
+
+        assertThat(starter2.hasLaunched()).isTrue();
+        assertThat(entry2.started()).isTrue();
+
+        runReturnTransition(starter2);
+
+        assertThat(exit2.started()).isTrue();
+
+        runReturnTransition(starter1);
+
+        assertThat(exit1.started()).isTrue();
+    }
+
+    private void runReturnTransition(FakeIntentStarter intentStarter) {
+        TransitionInfo info =
+                buildTransitionInfo(intentStarter.getToActivity(), intentStarter.getFromActivity());
+        mIOriginTransitions.runReturnTransition(intentStarter.getTransitionOfLastLaunch(), info);
+    }
+
+    private static TransitionInfo buildTransitionInfo(ComponentName from, ComponentName to) {
+        TransitionInfo info = new TransitionInfo(WindowManager.TRANSIT_OPEN, /* flags= */ 0);
+        TransitionInfo.Change c1 =
+                new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
+        c1.setMode(WindowManager.TRANSIT_OPEN);
+        c1.setActivityComponent(to);
+        TransitionInfo.Change c2 =
+                new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
+        c2.setMode(WindowManager.TRANSIT_CLOSE);
+        c2.setActivityComponent(from);
+        info.addChange(c2);
+        info.addChange(c1);
+        return info;
+    }
+
+    private static class FakeIntentStarter implements Predicate<RemoteTransition> {
+        private final ComponentName mFromActivity;
+        private final ComponentName mToActivity;
+        private final boolean mResult;
+
+        @Nullable private RemoteTransition mTransition;
+        private boolean mLaunched;
+
+        FakeIntentStarter(ComponentName from, ComponentName to) {
+            this(from, to, /* result= */ true);
+        }
+
+        FakeIntentStarter(ComponentName from, ComponentName to, boolean result) {
+            mFromActivity = from;
+            mToActivity = to;
+            mResult = result;
+        }
+
+        @Override
+        public boolean test(RemoteTransition transition) {
+            if (mResult) {
+                mLaunched = true;
+                mTransition = transition;
+                if (mTransition != null) {
+                    TransitionInfo info = buildTransitionInfo(mFromActivity, mToActivity);
+                    try {
+                        transition
+                                .getRemoteTransition()
+                                .startAnimation(
+                                        new Binder(),
+                                        info,
+                                        new SurfaceControl.Transaction(),
+                                        new FakeFinishCallback());
+                    } catch (RemoteException e) {
+
+                    }
+                }
+            }
+            return mResult;
+        }
+
+        @Nullable
+        public RemoteTransition getTransitionOfLastLaunch() {
+            return mTransition;
+        }
+
+        public ComponentName getFromActivity() {
+            return mFromActivity;
+        }
+
+        public ComponentName getToActivity() {
+            return mToActivity;
+        }
+
+        public boolean hasLaunched() {
+            return mLaunched;
+        }
+
+        public void reset() {
+            mTransition = null;
+            mLaunched = false;
+        }
+    }
+
+    private static class FakeIOriginTransitions extends IOriginTransitions.Stub {
+        private final Map<RemoteTransition, RemoteTransition> mRecords = new ArrayMap<>();
+
+        @Override
+        public RemoteTransition makeOriginTransition(
+                RemoteTransition launchTransition, RemoteTransition returnTransition) {
+            mRecords.put(launchTransition, returnTransition);
+            return launchTransition;
+        }
+
+        @Override
+        public void cancelOriginTransition(RemoteTransition originTransition) {
+            mRecords.remove(originTransition);
+        }
+
+        public void runReturnTransition(RemoteTransition originTransition, TransitionInfo info) {
+            RemoteTransition transition = mRecords.remove(originTransition);
+            try {
+                transition
+                        .getRemoteTransition()
+                        .startAnimation(
+                                new Binder(),
+                                info,
+                                new SurfaceControl.Transaction(),
+                                new FakeFinishCallback());
+            } catch (RemoteException e) {
+
+            }
+        }
+
+        public boolean hasPendingReturnTransitions() {
+            return !mRecords.isEmpty();
+        }
+    }
+
+    private static class FakeFinishCallback extends IRemoteTransitionFinishedCallback.Stub {
+        @Override
+        public void onTransitionFinished(
+                WindowContainerTransaction wct, SurfaceControl.Transaction sct) {}
+    }
+
+    private static class FakeRemoteTransition extends IRemoteTransition.Stub {
+        private boolean mStarted;
+
+        @Override
+        public void startAnimation(
+                IBinder token,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IRemoteTransitionFinishedCallback finishCallback)
+                throws RemoteException {
+            mStarted = true;
+            finishCallback.onTransitionFinished(null, null);
+        }
+
+        @Override
+        public void mergeAnimation(
+                IBinder transition,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IBinder mergeTarget,
+                IRemoteTransitionFinishedCallback finishCallback) {}
+
+        @Override
+        public void takeOverAnimation(
+                IBinder transition,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IRemoteTransitionFinishedCallback finishCallback,
+                WindowAnimationState[] states) {}
+
+        @Override
+        public void onTransitionConsumed(IBinder transition, boolean aborted) {}
+
+        public boolean started() {
+            return mStarted;
+        }
+
+        public void reset() {
+            mStarted = false;
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
index 2c8cc1a..3053672 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -47,7 +47,6 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.SystemClock
-import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -248,35 +247,32 @@
 
     @Test
     @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testShowNotification_removeWhenReorderingAllowedTrue() {
-        whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
-        val hmp = createHeadsUpManagerPhone()
-
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        hmp.showNotification(notifEntry)
-        assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)).isTrue();
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testShowNotification_reorderNotAllowed_seenInShadeTrue() {
+    fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() {
         whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
         val hmp = createHeadsUpManagerPhone()
 
         val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = mock<ExpandableNotificationRow>()
+        whenever(row.showingPulsing()).thenReturn(false)
+        notifEntry.row = row
+
         hmp.showNotification(notifEntry)
-        assertThat(notifEntry.isSeenInShade).isTrue();
+        Assert.assertTrue(notifEntry.isSeenInShade)
     }
 
     @Test
     @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testShowNotification_reorderAllowed_seenInShadeFalse() {
+    fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() {
         whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
         val hmp = createHeadsUpManagerPhone()
 
         val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = mock<ExpandableNotificationRow>()
+        whenever(row.showingPulsing()).thenReturn(false)
+        notifEntry.row = row
+
         hmp.showNotification(notifEntry)
-        assertThat(notifEntry.isSeenInShade).isFalse();
+        Assert.assertFalse(notifEntry.isSeenInShade)
     }
 
     @Test
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 6a8cc17..4f3ea83 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -126,12 +126,12 @@
     private val overlayColorActive =
         Utils.applyAlpha(
             /* alpha= */ 0.11f,
-            Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
+            Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive),
         )
     private val overlayColorInactive =
         Utils.applyAlpha(
             /* alpha= */ 0.08f,
-            Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive)
+            Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive),
         )
 
     private val colorLabelActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
@@ -188,10 +188,7 @@
     private var lastState = INVALID
     private var lastIconTint = 0
     private val launchableViewDelegate =
-        LaunchableViewDelegate(
-            this,
-            superSetVisibility = { super.setVisibility(it) },
-        )
+        LaunchableViewDelegate(this, superSetVisibility = { super.setVisibility(it) })
     private var lastDisabledByPolicy = false
 
     private val locInScreen = IntArray(2)
@@ -418,7 +415,7 @@
             initLongPressEffectCallback()
             init(
                 { _: View -> longPressEffect.onTileClick() },
-                null, // Haptics and long-clicks will be handled by the [QSLongPressEffect]
+                { _: View -> true }, // Haptics and long-clicks are handled by [QSLongPressEffect]
             )
         } else {
             val expandable = Expandable.fromView(this)
@@ -583,7 +580,7 @@
                     AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
                     resources.getString(
                         R.string.accessibility_tile_disabled_by_policy_action_description
-                    )
+                    ),
                 )
             )
         } else {
@@ -591,7 +588,7 @@
                 info.addAction(
                     AccessibilityNodeInfo.AccessibilityAction(
                         AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
-                        resources.getString(R.string.accessibility_long_click_tile)
+                        resources.getString(R.string.accessibility_long_click_tile),
                     )
                 )
             }
@@ -716,35 +713,35 @@
                 state.spec,
                 state.state,
                 state.disabledByPolicy,
-                getBackgroundColorForState(state.state, state.disabledByPolicy)
+                getBackgroundColorForState(state.state, state.disabledByPolicy),
             )
             if (allowAnimations) {
                 singleAnimator.setValues(
                     colorValuesHolder(
                         BACKGROUND_NAME,
                         backgroundColor,
-                        getBackgroundColorForState(state.state, state.disabledByPolicy)
+                        getBackgroundColorForState(state.state, state.disabledByPolicy),
                     ),
                     colorValuesHolder(
                         LABEL_NAME,
                         label.currentTextColor,
-                        getLabelColorForState(state.state, state.disabledByPolicy)
+                        getLabelColorForState(state.state, state.disabledByPolicy),
                     ),
                     colorValuesHolder(
                         SECONDARY_LABEL_NAME,
                         secondaryLabel.currentTextColor,
-                        getSecondaryLabelColorForState(state.state, state.disabledByPolicy)
+                        getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
                     ),
                     colorValuesHolder(
                         CHEVRON_NAME,
                         chevronView.imageTintList?.defaultColor ?: 0,
-                        getChevronColorForState(state.state, state.disabledByPolicy)
+                        getChevronColorForState(state.state, state.disabledByPolicy),
                     ),
                     colorValuesHolder(
                         OVERLAY_NAME,
                         backgroundOverlayColor,
-                        getOverlayColorForState(state.state)
-                    )
+                        getOverlayColorForState(state.state),
+                    ),
                 )
                 singleAnimator.start()
             } else {
@@ -753,7 +750,7 @@
                     getLabelColorForState(state.state, state.disabledByPolicy),
                     getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
                     getChevronColorForState(state.state, state.disabledByPolicy),
-                    getOverlayColorForState(state.state)
+                    getOverlayColorForState(state.state),
                 )
             }
         }
@@ -1077,7 +1074,7 @@
             backgroundColor,
             label.currentTextColor,
             secondaryLabel.currentTextColor,
-            chevronView.imageTintList?.defaultColor ?: 0
+            chevronView.imageTintList?.defaultColor ?: 0,
         )
 
     inner class StateChangeRunnable(private val state: QSTile.State) : Runnable {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
index 6907eef..1c840e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
@@ -103,8 +103,7 @@
     private boolean mTrackingHeadsUp;
     private final HashSet<String> mSwipedOutKeys = new HashSet<>();
     private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
-    @VisibleForTesting
-    public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
+    private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
             = new ArraySet<>();
     private boolean mIsShadeOrQsExpanded;
     private boolean mIsQsExpanded;
@@ -428,7 +427,7 @@
         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
             if (isHeadsUpEntry(entry.getKey())) {
                 // Maybe the heads-up was removed already
-                removeEntry(entry.getKey(), "allowReorder");
+                removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
             }
         }
         mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -631,8 +630,11 @@
             super.setEntry(entry, removeRunnable);
 
             if (NotificationThrottleHun.isEnabled()) {
-                mEntriesToRemoveWhenReorderingAllowed.add(entry);
-                if (!mVisualStabilityProvider.isReorderingAllowed()) {
+                if (!mVisualStabilityProvider.isReorderingAllowed()
+                        // We don't want to allow reordering while pulsing, but headsup need to
+                        // time out anyway
+                        && !entry.showingPulsing()) {
+                    mEntriesToRemoveWhenReorderingAllowed.add(entry);
                     entry.setSeenInShade(true);
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index caf09a3..674cbb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -46,8 +46,6 @@
 
     private val tag = "AvalancheController"
     private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
-    var baseEntryMapStr : () -> String = { "baseEntryMapStr not initialized" }
-
     var enableAtRuntime = true
         set(value) {
             if (!value) {
@@ -118,43 +116,32 @@
         val key = getKey(entry)
 
         if (runnable == null) {
-            headsUpManagerLogger.logAvalancheUpdate(
-                caller, isEnabled, key,
-                "Runnable NULL, stop. ${getStateStr()}"
-            )
+            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Runnable NULL, stop")
             return
         }
         if (!isEnabled) {
-            headsUpManagerLogger.logAvalancheUpdate(
-                caller, isEnabled, key,
-                "NOT ENABLED, run runnable. ${getStateStr()}"
-            )
+            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key,
+                    "NOT ENABLED, run runnable")
             runnable.run()
             return
         }
         if (entry == null) {
-            headsUpManagerLogger.logAvalancheUpdate(
-                caller, isEnabled, key,
-                "Entry NULL, stop. ${getStateStr()}"
-            )
+            headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Entry NULL, stop")
             return
         }
         if (debug) {
             debugRunnableLabelMap[runnable] = caller
         }
-        var stateAfter = ""
+        var outcome = ""
         if (isShowing(entry)) {
+            outcome = "update showing"
             runnable.run()
-            stateAfter = "update showing"
-
         } else if (entry in nextMap) {
+            outcome = "update next"
             nextMap[entry]?.add(runnable)
-            stateAfter = "update next"
-
         } else if (headsUpEntryShowing == null) {
+            outcome = "show now"
             showNow(entry, arrayListOf(runnable))
-            stateAfter = "show now"
-
         } else {
             // Clean up invalid state when entry is in list but not map and vice versa
             if (entry in nextMap) nextMap.remove(entry)
@@ -175,8 +162,8 @@
                 )
             }
         }
-        stateAfter += getStateStr()
-        headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled = true, key, stateAfter)
+        outcome += getStateStr()
+        headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome)
     }
 
     @VisibleForTesting
@@ -194,40 +181,32 @@
         val key = getKey(entry)
 
         if (runnable == null) {
-            headsUpManagerLogger.logAvalancheDelete(
-                caller, isEnabled, key,
-                "Runnable NULL, stop. ${getStateStr()}"
-            )
+            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key, "Runnable NULL, stop")
             return
         }
         if (!isEnabled) {
+            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
+                    "NOT ENABLED, run runnable")
             runnable.run()
-            headsUpManagerLogger.logAvalancheDelete(
-                caller, isEnabled = false, key,
-                "NOT ENABLED, run runnable. ${getStateStr()}"
-            )
             return
         }
         if (entry == null) {
+            headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
+                    "Entry NULL, run runnable")
             runnable.run()
-            headsUpManagerLogger.logAvalancheDelete(
-                caller, isEnabled = true, key,
-                "Entry NULL, run runnable. ${getStateStr()}"
-            )
             return
         }
-        val stateAfter: String
+        val outcome: String
         if (entry in nextMap) {
+            outcome = "remove from next"
             if (entry in nextMap) nextMap.remove(entry)
             if (entry in nextList) nextList.remove(entry)
             uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_REMOVED)
-            stateAfter = "remove from next. ${getStateStr()}"
-
         } else if (entry in debugDropSet) {
+            outcome = "remove from dropset"
             debugDropSet.remove(entry)
-            stateAfter = "remove from dropset. ${getStateStr()}"
-
         } else if (isShowing(entry)) {
+            outcome = "remove showing"
             previousHunKey = getKey(headsUpEntryShowing)
             // Show the next HUN before removing this one, so that we don't tell listeners
             // onHeadsUpPinnedModeChanged, which causes
@@ -235,13 +214,11 @@
             // HUN is animating out, resulting in a flicker.
             showNext()
             runnable.run()
-            stateAfter = "remove showing. ${getStateStr()}"
-
         } else {
+            outcome = "run runnable for untracked shown"
             runnable.run()
-            stateAfter = "run runnable for untracked shown HUN. ${getStateStr()}"
         }
-        headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), stateAfter)
+        headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome)
     }
 
     /**
@@ -423,14 +400,12 @@
     }
 
     private fun getStateStr(): String {
-        return "\nAvalancheController:" +
+        return "\navalanche state:" +
                 "\n\tshowing: [${getKey(headsUpEntryShowing)}]" +
                 "\n\tprevious: [$previousHunKey]" +
                 "\n\tnext list: $nextListStr" +
                 "\n\tnext map: $nextMapStr" +
-                "\n\tdropped: $dropSetStr" +
-                "\nBHUM.mHeadsUpEntryMap: " +
-                baseEntryMapStr()
+                "\n\tdropped: $dropSetStr"
     }
 
     private val dropSetStr: String
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 30524a5..f37393a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -116,7 +116,6 @@
         mAccessibilityMgr = accessibilityManagerWrapper;
         mUiEventLogger = uiEventLogger;
         mAvalancheController = avalancheController;
-        mAvalancheController.setBaseEntryMapStr(this::getEntryMapStr);
         Resources resources = context.getResources();
         mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
                 ? 500 : resources.getInteger(R.integer.heads_up_notification_minimum_time);
@@ -590,18 +589,6 @@
         dumpInternal(pw, args);
     }
 
-    private String getEntryMapStr() {
-        if (mHeadsUpEntryMap.isEmpty()) {
-            return "EMPTY";
-        }
-        StringBuilder entryMapStr = new StringBuilder();
-        for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
-            entryMapStr.append("\n\t").append(
-                    entry.mEntry == null ? "null" : entry.mEntry.getKey());
-        }
-        return entryMapStr.toString();
-    }
-
     protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
@@ -1005,6 +992,7 @@
          * Clear any pending removal runnables.
          */
         public void cancelAutoRemovalCallbacks(@Nullable String reason) {
+            mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
             Runnable runnable = () -> {
                 final boolean removed = cancelAutoRemovalCallbackInternal();
 
@@ -1013,7 +1001,6 @@
                 }
             };
             if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
-                mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
                 mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
             } else {
                 // Just removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 41112cb..600270c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -52,7 +52,7 @@
         caller: String,
         isEnabled: Boolean,
         notifEntryKey: String,
-        stateAfter: String
+        outcome: String
     ) {
         buffer.log(
             TAG,
@@ -60,7 +60,7 @@
             {
                 str1 = caller
                 str2 = notifEntryKey
-                str3 = stateAfter
+                str3 = outcome
                 bool1 = isEnabled
             },
             { "$str1\n\t=> AC[isEnabled:$bool1] update: $str2\n\t=> $str3" }
@@ -71,7 +71,7 @@
         caller: String,
         isEnabled: Boolean,
         notifEntryKey: String,
-        stateAfter: String
+        outcome: String
     ) {
         buffer.log(
             TAG,
@@ -79,7 +79,7 @@
             {
                 str1 = caller
                 str2 = notifEntryKey
-                str3 = stateAfter
+                str3 = outcome
                 bool1 = isEnabled
             },
             { "$str1\n\t=> AC[isEnabled:$bool1] delete: $str2\n\t=> $str3" }
@@ -136,7 +136,7 @@
                 str1 = entry.logKey
                 str2 = reason ?: "unknown"
             },
-            { "$str2 => request: cancelAutoRemovalCallbacks: $str1" }
+            { "request: cancel auto remove of $str1 reason: $str2" }
         )
     }
 
@@ -148,7 +148,7 @@
                 str1 = entry.logKey
                 str2 = reason ?: "unknown"
             },
-            { "$str2 => cancel auto remove: $str1" }
+            { "cancel auto remove of $str1 reason: $str2" }
         )
     }
 
@@ -161,7 +161,7 @@
                 str2 = reason
                 bool1 = isWaiting
             },
-            { "request: $str2 => removeEntry: $str1 isWaiting: $isWaiting" }
+            { "request: $str2 => remove entry $str1 isWaiting: $isWaiting" }
         )
     }
 
@@ -174,7 +174,7 @@
                 str2 = reason
                 bool1 = isWaiting
             },
-            { "$str2 => removeEntry: $str1 isWaiting: $isWaiting" }
+            { "$str2 => remove entry $str1 isWaiting: $isWaiting" }
         )
     }
 
@@ -216,12 +216,12 @@
                 str1 = logKey(key)
                 str2 = reason
             },
-            { "remove notif $str1 when headsUpEntry is null, reason: $str2" }
+            { "remove notification $str1 when headsUpEntry is null, reason: $str2" }
         )
     }
 
     fun logNotificationActuallyRemoved(entry: NotificationEntry) {
-        buffer.log(TAG, INFO, { str1 = entry.logKey }, { "removed: $str1 " })
+        buffer.log(TAG, INFO, { str1 = entry.logKey }, { "notification removed $str1 " })
     }
 
     fun logUpdateNotificationRequest(key: String, alert: Boolean, hasEntry: Boolean) {
@@ -233,7 +233,7 @@
                 bool1 = alert
                 bool2 = hasEntry
             },
-            { "request: update notif $str1 alert: $bool1 hasEntry: $bool2" }
+            { "request: update notification $str1 alert: $bool1 hasEntry: $bool2" }
         )
     }
 
@@ -246,7 +246,7 @@
                 bool1 = alert
                 bool2 = hasEntry
             },
-            { "update notif $str1 alert: $bool1 hasEntry: $bool2" }
+            { "update notification $str1 alert: $bool1 hasEntry: $bool2" }
         )
     }
 
@@ -281,7 +281,7 @@
                 bool1 = isPinned
                 str2 = reason
             },
-            { "$str2 => setEntryPinned[$bool1]: $str1" }
+            { "$str2 => set entry pinned $str1 pinned: $bool1" }
         )
     }
 
@@ -290,7 +290,7 @@
             TAG,
             INFO,
             { bool1 = hasPinnedNotification },
-            { "hasPinnedNotification[$bool1]" }
+            { "has pinned notification changed to $bool1" }
         )
     }
 
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 1e723b5..c8f8c2a 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -204,7 +204,8 @@
                                             serviceIntent,
                                             targetUser,
                                             safeExecuteAppFunctionCallback,
-                                            /* bindFlags= */ Context.BIND_AUTO_CREATE);
+                                            /* bindFlags= */ Context.BIND_AUTO_CREATE
+                                                    | Context.BIND_FOREGROUND_SERVICE);
                                 })
                         .exceptionally(
                                 ex -> {
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 89dfc73..12e8c57 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -380,10 +380,12 @@
         }
 
         /** Gets transports that need to be marked as restricted by the VCN from CarrierConfig */
+        // TODO: b/262269892 This method was created to perform experiments before the relevant API
+        // was exposed. Now it is obsolete and should be removed.
         @VisibleForTesting(visibility = Visibility.PRIVATE)
         public Set<Integer> getRestrictedTransportsFromCarrierConfig(
                 ParcelUuid subGrp, TelephonySubscriptionSnapshot lastSnapshot) {
-            if (!Build.IS_ENG && !Build.IS_USERDEBUG) {
+            if (!Build.isDebuggable()) {
                 return RESTRICTED_TRANSPORTS_DEFAULT;
             }
 
diff --git a/services/core/java/com/android/server/WiredAccessoryManager.java b/services/core/java/com/android/server/WiredAccessoryManager.java
index b271d7e..ab69cd1 100644
--- a/services/core/java/com/android/server/WiredAccessoryManager.java
+++ b/services/core/java/com/android/server/WiredAccessoryManager.java
@@ -118,8 +118,11 @@
             if (mInputManager.getSwitchState(-1, InputDevice.SOURCE_ANY, SW_LINEOUT_INSERT) == 1) {
                 switchValues |= SW_LINEOUT_INSERT_BIT;
             }
-            notifyWiredAccessoryChanged(0, switchValues,
-                    SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT);
+            notifyWiredAccessoryChanged(
+                    0,
+                    switchValues,
+                    SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT,
+                    true /*isSynchronous*/);
         }
 
 
@@ -135,7 +138,13 @@
     }
 
     @Override
-    public void notifyWiredAccessoryChanged(long whenNanos, int switchValues, int switchMask) {
+    public void notifyWiredAccessoryChanged(
+            long whenNanos, int switchValues, int switchMask) {
+        notifyWiredAccessoryChanged(whenNanos, switchValues, switchMask, false /*isSynchronous*/);
+    }
+
+    public void notifyWiredAccessoryChanged(
+            long whenNanos, int switchValues, int switchMask, boolean isSynchronous) {
         if (LOG) {
             Slog.v(TAG, "notifyWiredAccessoryChanged: when=" + whenNanos
                     + " bits=" + switchCodeToString(switchValues, switchMask)
@@ -172,8 +181,10 @@
                     break;
             }
 
-            updateLocked(NAME_H2W,
-                    (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset);
+            updateLocked(
+                    NAME_H2W,
+                    (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset,
+                    isSynchronous);
         }
     }
 
@@ -195,8 +206,9 @@
      *
      * @param newName  One of the NAME_xxx variables defined above.
      * @param newState 0 or one of the BIT_xxx variables defined above.
+     * @param isSynchronous boolean to determine whether should happen sync or async
      */
-    private void updateLocked(String newName, int newState) {
+    private void updateLocked(String newName, int newState, boolean isSynchronous) {
         // Retain only relevant bits
         int headsetState = newState & SUPPORTED_HEADSETS;
         int usb_headset_anlg = headsetState & BIT_USB_HEADSET_ANLG;
@@ -234,12 +246,15 @@
             return;
         }
 
-        mWakeLock.acquire();
-
-        Log.i(TAG, "MSG_NEW_DEVICE_STATE");
-        Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState,
-                mHeadsetState, "");
-        mHandler.sendMessage(msg);
+        if (isSynchronous) {
+            setDevicesState(headsetState, mHeadsetState, "");
+        } else {
+            mWakeLock.acquire();
+            Log.i(TAG, "MSG_NEW_DEVICE_STATE");
+            Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState,
+                    mHeadsetState, "");
+            mHandler.sendMessage(msg);
+        }
 
         mHeadsetState = headsetState;
     }
@@ -439,7 +454,10 @@
             for (int i = 0; i < mUEventInfo.size(); ++i) {
                 UEventInfo uei = mUEventInfo.get(i);
                 if (devPath.equals(uei.getDevPath())) {
-                    updateLocked(name, uei.computeNewHeadsetState(mHeadsetState, state));
+                    updateLocked(
+                            name,
+                            uei.computeNewHeadsetState(mHeadsetState, state),
+                            false /*isSynchronous*/);
                     return;
                 }
             }
@@ -550,7 +568,10 @@
             synchronized (mLock) {
                 int mask = maskAndState.first;
                 int state = maskAndState.second;
-                updateLocked(name, mHeadsetState & ~(mask & ~state) | (mask & state));
+                updateLocked(
+                        name,
+                        mHeadsetState & ~(mask & ~state) | (mask & state),
+                        false /*isSynchronous*/);
                 return;
             }
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a21a3f1..35323d6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -579,7 +579,7 @@
     static final int RESERVED_BYTES_PER_LOGCAT_LINE = 100;
 
     // How many seconds should the system wait before terminating the spawned logcat process.
-    static final int LOGCAT_TIMEOUT_SEC = 10;
+    static final int LOGCAT_TIMEOUT_SEC = Flags.logcatLongerTimeout() ? 15 : 10;
 
     // Necessary ApplicationInfo flags to mark an app as persistent
     static final int PERSISTENT_MASK =
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 75e9fad..15277ce 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -437,7 +437,6 @@
         mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context,
                 mPowerAttributor, mPowerProfile, mCpuScalingPolicies,
                 mPowerStatsStore, Clock.SYSTEM_CLOCK);
-        mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore);
         mDumpHelper = new BatteryStatsDumpHelperImpl(mBatteryUsageStatsProvider);
         mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map, mHandler);
         mConfigFile = new AtomicFile(new File(systemDir, "battery_usage_stats_config"));
@@ -504,6 +503,9 @@
     }
 
     public void systemServicesReady() {
+        mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore,
+                Flags.accumulateBatteryUsageStats());
+
         MultiStatePowerAttributor attributor = (MultiStatePowerAttributor) mPowerAttributor;
         mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU,
                 Flags.streamlinedBatteryStats());
@@ -1100,14 +1102,17 @@
                                     DEVICE_CONFIG_NAMESPACE,
                                     MIN_CONSUMED_POWER_THRESHOLD_KEY,
                                     0);
-                    final BatteryUsageStatsQuery query =
-                            new BatteryUsageStatsQuery.Builder()
-                                    .setMaxStatsAgeMs(0)
-                                    .includeProcessStateData()
-                                    .includeVirtualUids()
-                                    .setMinConsumedPowerThreshold(minConsumedPowerThreshold)
-                                    .build();
-                    bus = getBatteryUsageStats(List.of(query)).get(0);
+                    BatteryUsageStatsQuery.Builder query = new BatteryUsageStatsQuery.Builder()
+                            .setMaxStatsAgeMs(0)
+                            .includeProcessStateData()
+                            .includeVirtualUids()
+                            .setMinConsumedPowerThreshold(minConsumedPowerThreshold);
+
+                    if (Flags.accumulateBatteryUsageStats()) {
+                        query.accumulated();
+                    }
+
+                    bus = getBatteryUsageStats(List.of(query.build())).get(0);
                     final int pullResult =
                             new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data);
                     try {
@@ -3016,6 +3021,9 @@
         if (Flags.streamlinedBatteryStats()) {
             pw.println("  --sample: collect and dump a sample of stats for debugging purpose");
         }
+        if (Flags.accumulateBatteryUsageStats()) {
+            pw.println("  --accumulated: continuously accumulated since setup or reset-all");
+        }
         pw.println("  <package.name>: optional name of package to filter output by.");
         pw.println("  -h: print this help text.");
         pw.println("Battery stats (batterystats) commands:");
@@ -3083,7 +3091,7 @@
     }
 
     private void dumpUsageStats(FileDescriptor fd, PrintWriter pw, int model,
-            boolean proto) {
+            boolean proto, boolean accumulated) {
         awaitCompletion();
         syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL);
 
@@ -3097,6 +3105,9 @@
         if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
             builder.powerProfileModeledOnly();
         }
+        if (accumulated) {
+            builder.accumulated();
+        }
         BatteryUsageStatsQuery query = builder.build();
         synchronized (mStats) {
             mStats.prepareForDumpLocked();
@@ -3287,6 +3298,7 @@
                 } else if ("--usage".equals(arg)) {
                     int model = BatteryConsumer.POWER_MODEL_UNDEFINED;
                     boolean proto = false;
+                    boolean accumulated = false;
                     for (int j = i + 1; j < args.length; j++) {
                         switch (args[j]) {
                             case "--proto":
@@ -3309,9 +3321,12 @@
                                 }
                                 break;
                             }
+                            case "--accumulated":
+                                accumulated = true;
+                                break;
                         }
                     }
-                    dumpUsageStats(fd, pw, model, proto);
+                    dumpUsageStats(fd, pw, model, proto, accumulated);
                     return;
                 } else if ("--wakeups".equals(arg)) {
                     mCpuWakeupStats.dump(new IndentingPrintWriter(pw, "  "),
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 3334393..9b51b6a 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -194,4 +194,15 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
+}
+
+flag {
+    name: "logcat_longer_timeout"
+    namespace: "backstage_power"
+    description: "Wait longer during the logcat gathering operation"
+    bug: "292533246"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index ce92dfb..b421264 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -605,7 +605,11 @@
                 break;
             case BluetoothProfile.LE_AUDIO:
                 if (mLeAudio != null && mLeAudioCallback != null) {
-                    mLeAudio.unregisterCallback(mLeAudioCallback);
+                    try {
+                        mLeAudio.unregisterCallback(mLeAudioCallback);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Exception while unregistering callback for LE audio", e);
+                    }
                 }
                 mLeAudio = null;
                 mLeAudioCallback = null;
@@ -682,12 +686,21 @@
                     return;
                 }
                 if (mLeAudio != null && mLeAudioCallback != null) {
-                    mLeAudio.unregisterCallback(mLeAudioCallback);
+                    try {
+                        mLeAudio.unregisterCallback(mLeAudioCallback);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Exception while unregistering callback for LE audio", e);
+                    }
                 }
                 mLeAudio = (BluetoothLeAudio) proxy;
                 mLeAudioCallback = new MyLeAudioCallback();
-                mLeAudio.registerCallback(
-                            mContext.getMainExecutor(), mLeAudioCallback);
+                try{
+                    mLeAudio.registerCallback(
+                                mContext.getMainExecutor(), mLeAudioCallback);
+                } catch (Exception e) {
+                    mLeAudioCallback = null;
+                    Log.e(TAG, "Exception while registering callback for LE audio", e);
+                }
                 break;
             case BluetoothProfile.A2DP_SINK:
             case BluetoothProfile.LE_AUDIO_BROADCAST:
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index a1e5ebc..cf0c5b0 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -180,7 +180,8 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) {
+        if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
+                || event.getClassification() == MotionEvent.CLASSIFICATION_PINCH) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2bb2b7b..f0fb33e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4799,7 +4799,8 @@
                 userData.mImeBindingState.mFocusedWindowEditorInfo,
                 info.focusedWindowName, userData.mImeBindingState.mFocusedWindowSoftInputMode,
                 reason, userData.mInFullscreenMode, info.requestWindowName,
-                info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName));
+                info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName,
+                userId));
 
         if (statsToken != null) {
             mImeTrackerService.onImmsUpdate(statsToken, info.requestWindowName);
@@ -6132,8 +6133,7 @@
             dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
         }
 
-        // TODO(b/365868861): Make StartInputHistory, SoftInputShowHideHistory and ImeTracker per
-        //  user.
+        // TODO(b/365868861): Make StartInputHistory and ImeTracker multi-user aware.
         synchronized (ImfLock.class) {
             p.println("  mStartInputHistory:");
             mStartInputHistory.dump(pw, "    ");
diff --git a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
index 3023603..8445632 100644
--- a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
+++ b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.os.SystemClock;
 import android.view.WindowManager;
 import android.view.inputmethod.EditorInfo;
@@ -62,6 +63,8 @@
         final String mImeTargetNameFromWm;
         @Nullable
         final String mImeSurfaceParentName;
+        @UserIdInt
+        final int mImeUserId;
 
         Entry(ClientState client, EditorInfo editorInfo,
                 String focusedWindowName,
@@ -69,7 +72,7 @@
                 @SoftInputShowHideReason int reason,
                 boolean inFullscreenMode, String requestWindowName,
                 @Nullable String imeControlTargetName, @Nullable String imeTargetName,
-                @Nullable String imeSurfaceParentName) {
+                @Nullable String imeSurfaceParentName, @UserIdInt int imeUserId) {
             mClientState = client;
             mEditorInfo = editorInfo;
             mFocusedWindowName = focusedWindowName;
@@ -82,6 +85,7 @@
             mImeControlTargetName = imeControlTargetName;
             mImeTargetNameFromWm = imeTargetName;
             mImeSurfaceParentName = imeSurfaceParentName;
+            mImeUserId = imeUserId;
         }
     }
 
@@ -102,7 +106,8 @@
                 continue;
             }
             pw.print(prefix);
-            pw.println("SoftInputShowHide #" + entry.mSequenceNumber + ":");
+            pw.println("SoftInputShowHide[" + entry.mImeUserId + "] #"
+                    + entry.mSequenceNumber + ":");
 
             pw.print(prefix);
             pw.println("  time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index f6d9dc2..03a34f2 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -87,6 +87,7 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.SystemService.TargetUser;
 import com.android.server.ondeviceintelligence.callbacks.ListenableDownloadCallback;
 
 import java.io.FileDescriptor;
@@ -194,9 +195,13 @@
 
             mIsServiceEnabled = isServiceEnabled();
         }
+    }
 
-        //connect to remote services(if available) during boot phase.
-        if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
+    @Override
+    public void onUserUnlocked(@NonNull TargetUser user) {
+        Slog.d(TAG, "onUserUnlocked: " + user.getUserHandle());
+        //connect to remote services(if available) during boot.
+        if(user.getUserHandle().equals(UserHandle.SYSTEM)) {
             try {
                 ensureRemoteInferenceServiceInitialized();
                 ensureRemoteIntelligenceServiceInitialized();
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 4665a72..89ced12 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2208,10 +2208,10 @@
             return true;
         }
         boolean permissionGranted = requireFullPermission ? hasPermission(
-                Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
                 : (hasPermission(
-                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
-                        || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS));
+                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+                        || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS, callingUid));
         if (!permissionGranted) {
             if (Process.isIsolatedUid(callingUid) && isKnownIsolatedComputeApp(callingUid)) {
                 return checkIsolatedOwnerHasPermission(callingUid, requireFullPermission);
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 5653da0..8657de2 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -716,7 +716,7 @@
                     visiblePackages.add(info.getActivityInfo().packageName);
                 }
                 final List<ApplicationInfo> installedPackages =
-                        mPackageManagerInternal.getInstalledApplications(
+                        mPackageManagerInternal.getInstalledApplicationsCrossUser(
                                 /* flags= */ 0, user.getIdentifier(), callingUid);
                 for (ApplicationInfo applicationInfo : installedPackages) {
                     if (!visiblePackages.contains(applicationInfo.packageName)) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index ff9c3e5..611e0d8 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -3389,18 +3389,31 @@
                     return true;
                 }
                 // Does it contain a device admin for any user?
-                int[] users;
+                int[] allUsers = mUserManager.getUserIds();
+                int[] targetUsers;
                 if (userId == UserHandle.USER_ALL) {
-                    users = mUserManager.getUserIds();
+                    targetUsers = allUsers;
                 } else {
-                    users = new int[]{userId};
+                    targetUsers = new int[]{userId};
                 }
-                for (int i = 0; i < users.length; ++i) {
-                    if (dpm.packageHasActiveAdmins(packageName, users[i])) {
+
+                for (int i = 0; i < targetUsers.length; ++i) {
+                    if (dpm.packageHasActiveAdmins(packageName, targetUsers[i])) {
                         return true;
                     }
-                    if (isDeviceManagementRoleHolder(packageName, users[i])
-                            && dpmi.isUserOrganizationManaged(users[i])) {
+                }
+
+                // If a package is DMRH on a managed user, it should also be treated as an admin on
+                // that user. If that package is also a system package, it should also be protected
+                // on other users otherwise "uninstall updates" on an unmanaged user may break
+                // management on other users because apk version is shared between all users.
+                var packageState = snapshotComputer().getPackageStateInternal(packageName);
+                if (packageState == null) {
+                    return false;
+                }
+                for (int user : packageState.isSystem() ? allUsers : targetUsers) {
+                    if (isDeviceManagementRoleHolder(packageName, user)
+                            && dpmi.isUserOrganizationManaged(user)) {
                         return true;
                     }
                 }
diff --git a/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java b/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java
new file mode 100644
index 0000000..dd6d5db
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java
@@ -0,0 +1,67 @@
+/*
+ * 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.power.stats;
+
+import android.os.BatteryUsageStats;
+import android.util.IndentingPrintWriter;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+class AccumulatedBatteryUsageStatsSection extends PowerStatsSpan.Section {
+    public static final String TYPE = "accumulated-battery-usage-stats";
+    public static final long ID = Long.MAX_VALUE;
+
+    private final BatteryUsageStats.Builder mBatteryUsageStats;
+
+    AccumulatedBatteryUsageStatsSection(BatteryUsageStats.Builder batteryUsageStats) {
+        super(TYPE);
+        mBatteryUsageStats = batteryUsageStats;
+    }
+
+    public BatteryUsageStats.Builder getBatteryUsageStatsBuilder() {
+        return mBatteryUsageStats;
+    }
+
+    @Override
+    public void write(TypedXmlSerializer serializer) throws IOException {
+        mBatteryUsageStats.build().writeXml(serializer);
+    }
+
+    @Override
+    public void dump(IndentingPrintWriter ipw) {
+        mBatteryUsageStats.build().dump(ipw, "");
+    }
+
+    static class Reader implements PowerStatsSpan.SectionReader {
+        @Override
+        public String getType() {
+            return TYPE;
+        }
+
+        @Override
+        public PowerStatsSpan.Section read(String sectionType, TypedXmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            return new AccumulatedBatteryUsageStatsSection(
+                    BatteryUsageStats.createBuilderFromXml(parser));
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index cb8e1a0..3f1d9a3 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -509,6 +509,7 @@
     }
 
     private boolean mSaveBatteryUsageStatsOnReset;
+    private boolean mAccumulateBatteryUsageStats;
     private BatteryUsageStatsProvider mBatteryUsageStatsProvider;
     private PowerStatsStore mPowerStatsStore;
 
@@ -11975,10 +11976,12 @@
      */
     public void saveBatteryUsageStatsOnReset(
             @NonNull BatteryUsageStatsProvider batteryUsageStatsProvider,
-            @NonNull PowerStatsStore powerStatsStore) {
+            @NonNull PowerStatsStore powerStatsStore,
+            boolean accumulateBatteryUsageStats) {
         mSaveBatteryUsageStatsOnReset = true;
         mBatteryUsageStatsProvider = batteryUsageStatsProvider;
         mPowerStatsStore = powerStatsStore;
+        mAccumulateBatteryUsageStats = accumulateBatteryUsageStats;
     }
 
     @GuardedBy("this")
@@ -12179,29 +12182,33 @@
             return;
         }
 
-        final BatteryUsageStats batteryUsageStats;
-        synchronized (this) {
-            batteryUsageStats = mBatteryUsageStatsProvider.getBatteryUsageStats(this,
-                    new BatteryUsageStatsQuery.Builder()
-                            .setMaxStatsAgeMs(0)
-                            .includePowerModels()
-                            .includeProcessStateData()
-                            .build());
-        }
-
-        // TODO(b/188068523): BatteryUsageStats should use monotonic time for start and end
-        // Once that change is made, we will be able to use the BatteryUsageStats' monotonic
-        // start time
-        long monotonicStartTime =
-                mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
-        mHandler.post(() -> {
-            mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
-            try {
-                batteryUsageStats.close();
-            } catch (IOException e) {
-                Log.e(TAG, "Cannot close BatteryUsageStats", e);
+        if (mAccumulateBatteryUsageStats) {
+            mBatteryUsageStatsProvider.accumulateBatteryUsageStats(this);
+        } else {
+            final BatteryUsageStats batteryUsageStats;
+            synchronized (this) {
+                batteryUsageStats = mBatteryUsageStatsProvider.getBatteryUsageStats(this,
+                        new BatteryUsageStatsQuery.Builder()
+                                .setMaxStatsAgeMs(0)
+                                .includePowerModels()
+                                .includeProcessStateData()
+                                .build());
             }
-        });
+
+            // TODO(b/188068523): BatteryUsageStats should use monotonic time for start and end
+            // Once that change is made, we will be able to use the BatteryUsageStats' monotonic
+            // start time
+            long monotonicStartTime =
+                    mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
+            mHandler.post(() -> {
+                mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
+                try {
+                    batteryUsageStats.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Cannot close BatteryUsageStats", e);
+                }
+            });
+        }
     }
 
     @GuardedBy("this")
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index 87a3e5e..d66e05b 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -33,6 +33,7 @@
 import com.android.internal.os.CpuScalingPolicies;
 import com.android.internal.os.PowerProfile;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -64,6 +65,7 @@
         mClock = clock;
 
         mPowerStatsStore.addSectionReader(new BatteryUsageStatsSection.Reader());
+        mPowerStatsStore.addSectionReader(new AccumulatedBatteryUsageStatsSection.Reader());
     }
 
     private List<PowerCalculator> getPowerCalculators() {
@@ -151,6 +153,56 @@
     }
 
     /**
+     * Compute BatteryUsageStats for the period since the last accumulated stats were stored,
+     * add them to the accumulated stats and save the result.
+     */
+    public void accumulateBatteryUsageStats(BatteryStatsImpl stats) {
+        BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+
+        PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+                AccumulatedBatteryUsageStatsSection.ID,
+                AccumulatedBatteryUsageStatsSection.TYPE);
+        if (powerStatsSpan != null) {
+            List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
+            for (int i = sections.size() - 1; i >= 0; i--) {
+                PowerStatsSpan.Section section = sections.get(i);
+                if (AccumulatedBatteryUsageStatsSection.TYPE.equals(section.getType())) {
+                    accumulatedBatteryUsageStatsBuilder =
+                            ((AccumulatedBatteryUsageStatsSection) section)
+                                    .getBatteryUsageStatsBuilder();
+                    break;
+                }
+            }
+        }
+
+        // TODO(b/366493365): add the current batteryusagestats directly into the "accumulated"
+        // builder to avoid allocating a second CursorWindow
+        BatteryUsageStats.Builder currentBatteryUsageStatsBuilder =
+                getCurrentBatteryUsageStatsBuilder(stats,
+                        new BatteryUsageStatsQuery.Builder()
+                                .setMaxStatsAgeMs(0)
+                                .includeProcessStateData()
+                                .includePowerStateData()
+                                .includeScreenStateData()
+                                .build(),
+                        mClock.currentTimeMillis());
+
+        if (accumulatedBatteryUsageStatsBuilder == null) {
+            accumulatedBatteryUsageStatsBuilder = currentBatteryUsageStatsBuilder;
+        } else {
+            accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStatsBuilder.build());
+            currentBatteryUsageStatsBuilder.discard();
+        }
+
+        powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID);
+        powerStatsSpan.addSection(
+                new AccumulatedBatteryUsageStatsSection(accumulatedBatteryUsageStatsBuilder));
+
+        mPowerStatsStore.storePowerStatsSpanAsync(powerStatsSpan,
+                accumulatedBatteryUsageStatsBuilder::discard);
+    }
+
+    /**
      * Returns true if the last update was too long ago for the tolerances specified
      * by the supplied queries.
      */
@@ -192,15 +244,67 @@
 
     private BatteryUsageStats getBatteryUsageStats(BatteryStatsImpl stats,
             BatteryUsageStatsQuery query, long currentTimeMs) {
-        if (query.getToTimestamp() == 0) {
+        if ((query.getFlags()
+                & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_ACCUMULATED) != 0) {
+            return getAccumulatedBatteryUsageStats(stats, query);
+        } else if (query.getToTimestamp() == 0) {
             return getCurrentBatteryUsageStats(stats, query, currentTimeMs);
         } else {
             return getAggregatedBatteryUsageStats(stats, query);
         }
     }
 
+    private BatteryUsageStats getAccumulatedBatteryUsageStats(BatteryStatsImpl stats,
+            BatteryUsageStatsQuery query) {
+        PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+                AccumulatedBatteryUsageStatsSection.ID,
+                AccumulatedBatteryUsageStatsSection.TYPE);
+
+        BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+        if (powerStatsSpan != null) {
+            List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
+            if (sections.size() == 1) {
+                accumulatedBatteryUsageStatsBuilder =
+                        ((AccumulatedBatteryUsageStatsSection) sections.get(0))
+                                .getBatteryUsageStatsBuilder();
+            } else {
+                Slog.wtf(TAG, "Unexpected number of sections for type "
+                        + AccumulatedBatteryUsageStatsSection.TYPE);
+            }
+        }
+
+        BatteryUsageStats currentBatteryUsageStats = getCurrentBatteryUsageStats(stats, query,
+                mClock.currentTimeMillis());
+
+        BatteryUsageStats result;
+        if (accumulatedBatteryUsageStatsBuilder == null) {
+            result = currentBatteryUsageStats;
+        } else {
+            accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStats);
+            try {
+                currentBatteryUsageStats.close();
+            } catch (IOException ex) {
+                Slog.e(TAG, "Closing BatteryUsageStats", ex);
+            }
+            result = accumulatedBatteryUsageStatsBuilder.build();
+        }
+
+        return result;
+    }
+
     private BatteryUsageStats getCurrentBatteryUsageStats(BatteryStatsImpl stats,
             BatteryUsageStatsQuery query, long currentTimeMs) {
+        BatteryUsageStats.Builder builder = getCurrentBatteryUsageStatsBuilder(stats, query,
+                currentTimeMs);
+        BatteryUsageStats batteryUsageStats = builder.build();
+        if (batteryUsageStats.isProcessStateDataIncluded()) {
+            verify(batteryUsageStats);
+        }
+        return batteryUsageStats;
+    }
+
+    private BatteryUsageStats.Builder getCurrentBatteryUsageStatsBuilder(BatteryStatsImpl stats,
+            BatteryUsageStatsQuery query, long currentTimeMs) {
         final long realtimeUs = mClock.elapsedRealtime() * 1000;
         final long uptimeUs = mClock.uptimeMillis() * 1000;
 
@@ -274,11 +378,7 @@
         mPowerAttributor.estimatePowerConsumption(batteryUsageStatsBuilder, stats.getHistory(),
                 monotonicStartTime, monotonicEndTime);
 
-        BatteryUsageStats batteryUsageStats = batteryUsageStatsBuilder.build();
-        if (includeProcessStateData) {
-            verify(batteryUsageStats);
-        }
-        return batteryUsageStats;
+        return batteryUsageStatsBuilder;
     }
 
     // STOPSHIP(b/229906525): remove verification before shipping
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsStore.java b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
index a875c30..5a6f973 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsStore.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
@@ -133,6 +133,19 @@
     }
 
     /**
+     * Schedules saving the specified span on the background thread.
+     */
+    public void storePowerStatsSpanAsync(PowerStatsSpan span, Runnable onComplete) {
+        mHandler.post(() -> {
+            try {
+                storePowerStatsSpan(span);
+            } finally {
+                onComplete.run();
+            }
+        });
+    }
+
+    /**
      * Saves the specified span in the store.
      */
     public void storePowerStatsSpan(PowerStatsSpan span) {
@@ -172,6 +185,9 @@
         lockStoreDirectory();
         try {
             File file = makePowerStatsSpanFilename(id);
+            if (!file.exists()) {
+                return null;
+            }
             try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
                 return PowerStatsSpan.read(inputStream, parser, mSectionReaders, sectionTypes);
             } catch (IOException | XmlPullParserException e) {
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index 05d29f5..ce6f57f 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -76,3 +76,13 @@
     bug: "364350206"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "accumulate_battery_usage_stats"
+    namespace: "backstage_power"
+    description: "Add support for monotonically accumulated BatteryUsageStats"
+    bug: "345022340"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 090e679..5b22c10 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -16,6 +16,8 @@
 
 package com.android.server.vibrator;
 
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.PowerManager;
@@ -117,7 +119,7 @@
      *  before the release callback.
      */
     boolean runVibrationOnVibrationThread(VibrationStepConductor conductor) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread");
         try {
             synchronized (mLock) {
                 if (mRequestedActiveConductor != null) {
@@ -129,7 +131,7 @@
             }
             return true;
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -249,17 +251,17 @@
     private void clientVibrationCompleteIfNotAlready(@NonNull Vibration.EndInfo vibrationEndInfo) {
         if (!mCalledVibrationCompleteCallback) {
             mCalledVibrationCompleteCallback = true;
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
             try {
                 mExecutingConductor.notifyVibrationComplete(vibrationEndInfo);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
     }
 
     private void playVibration() {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "playVibration");
         try {
             mExecutingConductor.prepareToStart();
             while (!mExecutingConductor.isFinished()) {
@@ -283,7 +285,7 @@
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 3c47850..c120fc7 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -16,6 +16,8 @@
 
 package com.android.server.vibrator;
 
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
 import android.annotation.Nullable;
 import android.hardware.vibrator.IVibrator;
 import android.os.Binder;
@@ -124,7 +126,7 @@
 
     /** Reruns the query to the vibrator to load the {@link VibratorInfo}, if not yet successful. */
     public void reloadVibratorInfoIfNeeded() {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded");
         try {
             // Early check outside lock, for quick return.
             if (mVibratorInfoLoadSuccessful) {
@@ -143,7 +145,7 @@
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -199,13 +201,13 @@
 
     /** Return {@code true} if the underlying vibrator is currently available, false otherwise. */
     public boolean isAvailable() {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#isAvailable");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#isAvailable");
         try {
             synchronized (mLock) {
                 return mNativeWrapper.isAvailable();
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -215,7 +217,9 @@
      * <p>This will affect the state of {@link #isUnderExternalControl()}.
      */
     public void setExternalControl(boolean externalControl) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setExternalControl(" + externalControl + ")");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR,
+                externalControl ? "VibratorController#enableExternalControl"
+                : "VibratorController#disableExternalControl");
         try {
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
                 return;
@@ -225,7 +229,7 @@
                 mNativeWrapper.setExternalControl(externalControl);
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -234,7 +238,7 @@
      * if given {@code effect} is {@code null}.
      */
     public void updateAlwaysOn(int id, @Nullable PrebakedSegment prebaked) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn");
         try {
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) {
                 return;
@@ -248,13 +252,13 @@
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
     /** Set the vibration amplitude. This will NOT affect the state of {@link #isVibrating()}. */
     public void setAmplitude(float amplitude) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude");
         try {
             synchronized (mLock) {
                 if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) {
@@ -265,7 +269,7 @@
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -279,7 +283,7 @@
      * do not support the input or a negative number if the operation failed.
      */
     public long on(long milliseconds, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on");
         try {
             synchronized (mLock) {
                 long duration = mNativeWrapper.on(milliseconds, vibrationId);
@@ -290,7 +294,7 @@
                 return duration;
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -304,7 +308,7 @@
      * do not support the input or a negative number if the operation failed.
      */
     public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)");
         synchronized (mLock) {
             Parcel vendorData = Parcel.obtain();
             try {
@@ -320,7 +324,7 @@
                 return duration;
             } finally {
                 vendorData.recycle();
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
     }
@@ -335,7 +339,7 @@
      * do not support the input or a negative number if the operation failed.
      */
     public long on(PrebakedSegment prebaked, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)");
         try {
             synchronized (mLock) {
                 long duration = mNativeWrapper.perform(prebaked.getEffectId(),
@@ -347,7 +351,7 @@
                 return duration;
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -361,7 +365,7 @@
      * do not support the input or a negative number if the operation failed.
      */
     public long on(PrimitiveSegment[] primitives, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)");
         try {
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
                 return 0;
@@ -375,7 +379,7 @@
                 return duration;
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -388,7 +392,7 @@
      * @return The duration of the effect playing, or 0 if unsupported.
      */
     public long on(RampSegment[] primitives, long vibrationId) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)");
         try {
             if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) {
                 return 0;
@@ -403,7 +407,7 @@
                 return duration;
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -413,7 +417,7 @@
      * <p>This will affect the state of {@link #isVibrating()}.
      */
     public void off() {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#off");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#off");
         try {
             synchronized (mLock) {
                 mNativeWrapper.off();
@@ -421,7 +425,7 @@
                 notifyListenerOnVibrating(false);
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index bd17e5d..95c6483 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.vibrator;
 
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
 import static android.os.VibrationAttributes.USAGE_CLASS_ALARM;
 import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
 import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
@@ -333,7 +334,7 @@
     @VisibleForTesting
     void systemReady() {
         Slog.v(TAG, "Initializing VibratorManager service...");
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "systemReady");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "systemReady");
         try {
             // Will retry to load each vibrator's info, if any request have failed.
             for (int i = 0; i < mVibrators.size(); i++) {
@@ -352,7 +353,7 @@
                 mServiceReady = true;
             }
             Slog.v(TAG, "VibratorManager service initialized");
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -413,7 +414,7 @@
     @Override // Binder call
     public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId,
             @Nullable CombinedVibration effect, @Nullable VibrationAttributes attrs) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
         try {
             mContext.enforceCallingOrSelfPermission(
                     android.Manifest.permission.VIBRATE_ALWAYS_ON,
@@ -449,20 +450,25 @@
             }
             return true;
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
     @Override // Binder call
     public void vibrate(int uid, int deviceId, String opPkg, @NonNull CombinedVibration effect,
             @Nullable VibrationAttributes attrs, String reason, IBinder token) {
-        vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "vibrate");
+        try {
+            vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
+        } finally {
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
+        }
     }
 
     @Override // Binder call
     public void performHapticFeedback(int uid, int deviceId, String opPkg, int constant,
             String reason, int flags, int privFlags) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedback");
         // Note that the `performHapticFeedback` method does not take a token argument from the
         // caller, and instead, uses this service as the token. This is to mitigate performance
         // impact that would otherwise be caused due to marshal latency. Haptic feedback effects are
@@ -471,7 +477,7 @@
             performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */
                     this, flags, privFlags);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -479,13 +485,13 @@
     public void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg,
             int constant, int inputDeviceId, int inputSource, String reason, int flags,
             int privFlags) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
         try {
             performHapticFeedbackForInputDeviceInternal(uid, deviceId, opPkg, constant,
                     inputDeviceId,
                     inputSource, reason, /* token= */ this, flags, privFlags);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -563,26 +569,16 @@
     HalVibration vibrateWithPermissionCheck(int uid, int deviceId, String opPkg,
             @NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
             String reason, IBinder token) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason);
-        try {
-            attrs = fixupVibrationAttributes(attrs, effect);
-            mContext.enforceCallingOrSelfPermission(
-                    android.Manifest.permission.VIBRATE, "vibrate");
-            return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-        }
+        attrs = fixupVibrationAttributes(attrs, effect);
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.VIBRATE, "vibrate");
+        return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
     }
 
     HalVibration vibrateWithoutPermissionCheck(int uid, int deviceId, String opPkg,
             @NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs,
             String reason, IBinder token) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate no perm check, reason = " + reason);
-        try {
-            return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
-        }
+        return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
     }
 
     private HalVibration vibrateInternal(int uid, int deviceId, String opPkg,
@@ -671,7 +667,7 @@
 
     @Override // Binder call
     public void cancelVibrate(int usageFilter, IBinder token) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelVibrate");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelVibrate");
         try {
             mContext.enforceCallingOrSelfPermission(
                     android.Manifest.permission.VIBRATE,
@@ -708,7 +704,7 @@
                 }
             }
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -899,7 +895,7 @@
     @GuardedBy("mLock")
     @Nullable
     private Vibration.EndInfo startVibrationLocked(HalVibration vib) {
-        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationLocked");
         try {
             if (mInputDeviceDelegate.isAvailable()) {
                 return startVibrationOnInputDevicesLocked(vib);
@@ -919,7 +915,7 @@
             mNextVibration = conductor;
             return null;
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
         }
     }
 
@@ -930,7 +926,7 @@
         int mode = startAppOpModeLocked(vib.callerInfo);
         switch (mode) {
             case AppOpsManager.MODE_ALLOWED:
-                Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+                Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0);
                 // Make sure mCurrentVibration is set while triggering the VibrationThread.
                 mCurrentVibration = conductor;
                 if (!mCurrentVibration.linkToDeath()) {
@@ -1581,7 +1577,7 @@
 
         @Override
         public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "prepareSyncedVibration");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "prepareSyncedVibration");
             try {
                 if ((mCapabilities & requiredCapabilities) != requiredCapabilities) {
                     // This sync step requires capabilities this device doesn't have, skipping
@@ -1590,33 +1586,33 @@
                 }
                 return mNativeWrapper.prepareSynced(vibratorIds);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public boolean triggerSyncedVibration(long vibrationId) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "triggerSyncedVibration");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "triggerSyncedVibration");
             try {
                 return mNativeWrapper.triggerSynced(vibrationId);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public void cancelSyncedVibration() {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelSyncedVibration");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelSyncedVibration");
             try {
                 mNativeWrapper.cancelSynced();
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public void noteVibratorOn(int uid, long duration) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOn");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOn");
             try {
                 if (duration <= 0) {
                     // Tried to turn vibrator ON and got:
@@ -1635,20 +1631,20 @@
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error logging VibratorStateChanged to ON", e);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public void noteVibratorOff(int uid) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOff");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOff");
             try {
                 mBatteryStatsService.noteVibratorOff(uid);
                 mFrameworkStatsLogger.writeVibratorStateOffAsync(uid);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error logging VibratorStateChanged to OFF", e);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
@@ -1657,7 +1653,8 @@
             if (DEBUG) {
                 Slog.d(TAG, "VibrationThread released after finished vibration");
             }
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationThreadReleased: " + vibrationId);
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased");
+
             try {
                 synchronized (mLock) {
                     if (DEBUG) {
@@ -1686,7 +1683,7 @@
                     }
                 }
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
     }
@@ -1994,7 +1991,7 @@
 
         @Override
         public ExternalVibrationScale onExternalVibrationStart(ExternalVibration vib) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStart");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStart");
             try {
                 // Create Vibration.Stats as close to the received request as possible, for
                 // tracking.
@@ -2116,13 +2113,13 @@
                     return externalVibration.getScale();
                 }
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
         @Override
         public void onExternalVibrationStop(ExternalVibration vib) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
             try {
                 synchronized (mLock) {
                     if (mCurrentExternalVibration != null
@@ -2135,7 +2132,7 @@
                     }
                 }
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
@@ -2203,32 +2200,39 @@
 
         @Override
         public int onCommand(String cmd) {
-            Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onCommand " + cmd);
             try {
                 if ("list".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: list");
                     return runListVibrators();
                 }
                 if ("synced".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: synced");
                     return runMono();
                 }
                 if ("combined".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: combined");
                     return runStereo();
                 }
                 if ("sequential".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: sequential");
                     return runSequential();
                 }
                 if ("xml".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: xml");
                     return runXml();
                 }
                 if ("cancel".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: cancel");
                     return runCancel();
                 }
                 if ("feedback".equals(cmd)) {
+                    Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: feedback");
                     return runHapticFeedback();
                 }
+                Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: default");
                 return handleDefaultCommands(cmd);
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
             }
         }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 407a5a6..b6a4481 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -19384,11 +19384,13 @@
                     PolicyDefinition.RESET_PASSWORD_TOKEN,
                     enforcingAdmin,
                     userId);
-            // TODO(b/369152176): Address difference in behavior regarding addEscrowToken when
-            //  compared with the else branch.
             long tokenHandle = addEscrowToken(
                     token, currentTokenHandle == null ? 0 : currentTokenHandle, userId);
             if (tokenHandle == 0) {
+                mDevicePolicyEngine.removeLocalPolicy(
+                        PolicyDefinition.RESET_PASSWORD_TOKEN,
+                        enforcingAdmin,
+                        userId);
                 return false;
             }
             mDevicePolicyEngine.setLocalPolicy(
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
new file mode 100644
index 0000000..fead05b
--- /dev/null
+++ b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
@@ -0,0 +1,46 @@
+/*
+ * 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.supervision;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.Bundle;
+
+/**
+ * Local system service interface for {@link SupervisionService}.
+ *
+ * @hide Only for use within Android OS.
+ */
+public abstract class SupervisionManagerInternal {
+    /**
+     * Returns whether supervision is enabled for the specified user
+     *
+     * @param userId The user to retrieve the supervision state for
+     * @return whether the user is supervised
+     */
+    public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId);
+
+    /**
+     * Sets whether the supervision lock screen should be shown for the specified user
+     *
+     * @param userId The user set the superivision state for
+     * @param enabled Whether or not the superivision lock screen needs to be shown
+     * @param options Optional configuration parameters for the supervision lock screen
+     */
+    public abstract void setSupervisionLockscreenEnabledForUser(
+            @UserIdInt int userId, boolean enabled, @Nullable Bundle options);
+}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 7ffd0ec..4c515c1 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -18,14 +18,22 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.supervision.ISupervisionManager;
 import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
+import android.util.SparseArray;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -38,13 +46,25 @@
 
     private final Context mContext;
 
+    // TODO(b/362756788): Does this need to be a LockGuard lock?
+    private final Object mLockDoNoUseDirectly = new Object();
+
+    @GuardedBy("getLockObject()")
+    private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>();
+
+    private final UserManagerInternal mUserManagerInternal;
+
     public SupervisionService(Context context) {
-        mContext = context.createAttributionContext("SupervisionService");
+        mContext = context.createAttributionContext(LOG_TAG);
+        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+        mUserManagerInternal.addUserLifecycleListener(new UserLifecycleListener());
     }
 
     @Override
-    public boolean isSupervisionEnabled() {
-        return false;
+    public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+        synchronized (getLockObject()) {
+            return getUserDataLocked(userId).supervisionEnabled;
+        }
     }
 
     @Override
@@ -60,11 +80,44 @@
     }
 
     @Override
-    protected void dump(
-            @NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) {
-        if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, fout)) return;
+    protected void dump(@NonNull FileDescriptor fd,
+            @NonNull PrintWriter printWriter, @Nullable String[] args) {
+        if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, printWriter)) return;
 
-        fout.println("Supervision enabled: " + isSupervisionEnabled());
+        try (var pw = new IndentingPrintWriter(printWriter, "  ")) {
+            pw.println("SupervisionService state:");
+            pw.increaseIndent();
+
+            var users = mUserManagerInternal.getUsers(false);
+            synchronized (getLockObject()) {
+                for (var user : users) {
+                    getUserDataLocked(user.id).dump(pw);
+                    pw.println();
+                }
+            }
+        }
+    }
+
+    private Object getLockObject() {
+        return mLockDoNoUseDirectly;
+    }
+
+    @NonNull
+    @GuardedBy("getLockObject()")
+    SupervisionUserData getUserDataLocked(@UserIdInt int userId) {
+        SupervisionUserData data = mUserData.get(userId);
+        if (data == null) {
+            // TODO(b/362790738): Do not create user data for nonexistent users.
+            data = new SupervisionUserData(userId);
+            mUserData.append(userId, data);
+        }
+        return data;
+    }
+
+    void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+        synchronized (getLockObject()) {
+            getUserDataLocked(userId).supervisionEnabled = enabled;
+        }
     }
 
     public static class Lifecycle extends SystemService {
@@ -77,7 +130,35 @@
 
         @Override
         public void onStart() {
+            publishLocalService(SupervisionManagerInternal.class, mSupervisionService.mInternal);
             publishBinderService(Context.SUPERVISION_SERVICE, mSupervisionService);
         }
     }
+
+    final SupervisionManagerInternal mInternal = new SupervisionManagerInternal() {
+        public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+            synchronized (getLockObject()) {
+                return getUserDataLocked(userId).supervisionEnabled;
+            }
+        }
+
+        @Override
+        public void setSupervisionLockscreenEnabledForUser(
+                @UserIdInt int userId, boolean enabled, @Nullable Bundle options) {
+            synchronized (getLockObject()) {
+                SupervisionUserData data = getUserDataLocked(userId);
+                data.supervisionLockScreenEnabled = enabled;
+                data.supervisionLockScreenOptions = options;
+            }
+        }
+    };
+
+    private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener {
+        @Override
+        public void onUserRemoved(UserInfo user) {
+            synchronized (getLockObject()) {
+                mUserData.remove(user.id);
+            }
+        }
+    }
 }
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
index 3aba24a..2adaae3 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
@@ -17,8 +17,7 @@
 package com.android.server.supervision;
 
 import android.os.ShellCommand;
-
-import java.io.PrintWriter;
+import android.os.UserHandle;
 
 public class SupervisionServiceShellCommand extends ShellCommand {
     private final SupervisionService mService;
@@ -32,30 +31,29 @@
         if (cmd == null) {
             return handleDefaultCommands(null);
         }
-        final PrintWriter pw = getOutPrintWriter();
         switch (cmd) {
-            case "help": return help(pw);
-            case "is-enabled": return isEnabled(pw);
+            case "enable": return setEnabled(true);
+            case "disable": return setEnabled(false);
             default: return handleDefaultCommands(cmd);
         }
     }
 
-    private int help(PrintWriter pw) {
-        pw.println("Supervision service commands:");
-        pw.println("  help");
-        pw.println("      Prints this help text");
-        pw.println("  is-enabled");
-        pw.println("      Is supervision enabled");
-        return 0;
-    }
-
-    private int isEnabled(PrintWriter pw) {
-        pw.println(mService.isSupervisionEnabled());
+    private int setEnabled(boolean enabled) {
+        final var pw = getOutPrintWriter();
+        final var userId = UserHandle.parseUserArg(getNextArgRequired());
+        mService.setSupervisionEnabledForUser(userId, enabled);
         return 0;
     }
 
     @Override
     public void onHelp() {
-        help(getOutPrintWriter());
+        final var pw = getOutPrintWriter();
+        pw.println("Supervision service (supervision) commands:");
+        pw.println("  help");
+        pw.println("      Prints this help text");
+        pw.println("  enable <USER_ID>");
+        pw.println("      Enables supervision for the given user.");
+        pw.println("  disable <USER_ID>");
+        pw.println("      Disables supervision for the given user.");
     }
 }
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
new file mode 100644
index 0000000..5616237
--- /dev/null
+++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
@@ -0,0 +1,45 @@
+/*
+ * 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.supervision;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.Bundle;
+import android.util.IndentingPrintWriter;
+
+/** User specific data, used internally by the {@link SupervisionService}. */
+public class SupervisionUserData {
+    public final @UserIdInt int userId;
+    public boolean supervisionEnabled;
+    public boolean supervisionLockScreenEnabled;
+    @Nullable public Bundle supervisionLockScreenOptions;
+
+    public SupervisionUserData(@UserIdInt int userId) {
+        this.userId = userId;
+    }
+
+    void dump(@NonNull IndentingPrintWriter pw) {
+        pw.println();
+        pw.println("User " + userId + ":");
+        pw.increaseIndent();
+        pw.println("supervisionEnabled: " + supervisionEnabled);
+        pw.println("supervisionLockScreenEnabled: " + supervisionLockScreenEnabled);
+        pw.println("supervisionLockScreenOptions: " + supervisionLockScreenOptions);
+        pw.decreaseIndent();
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
index e5d3153..72cbac3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
@@ -35,6 +35,7 @@
 public final class InputMethodManagerServiceTests {
     static final int SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 2;
     static final int NO_SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 3;
+    private static final int TEST_IME_USER_ID = 1;
 
     static InputMethodManagerService.ImeDisplayValidator sChecker =
             (displayId) -> {
@@ -102,7 +103,8 @@
                 null,
                 null,
                 null,
-                null));
+                null,
+                TEST_IME_USER_ID));
 
         history.dump(new PrintWriter(writer), "" /* prefix */);
 
diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
index 5c4716d..7d5532f 100644
--- a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
+++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
@@ -57,6 +57,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.doReturn
@@ -383,6 +384,10 @@
                     android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
                 PackageManager.PERMISSION_GRANTED
             }
+            whenever(this.checkPermission(
+                eq(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL), anyInt(), anyInt())) {
+                PackageManager.PERMISSION_GRANTED
+            }
         }
         val mockSharedLibrariesImpl: SharedLibrariesImpl = mock {
             whenever(this.snapshot()) { this@mock }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 1d20538..c037f97 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -927,7 +927,7 @@
         assertThat(mPowerStatsStore.getTableOfContents()).isEmpty();
 
         mBatteryStatsImpl.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider,
-                mPowerStatsStore);
+                mPowerStatsStore, /* accumulateBatteryUsageStats */ false);
 
         synchronized (mBatteryStatsImpl) {
             mBatteryStatsImpl.noteFlashlightOnLocked(42, mMockClock.realtime, mMockClock.uptime);
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 fde84e9..0e60156 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
@@ -419,7 +419,8 @@
                 mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
                 mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
 
-        batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+        batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+                /* accumulateBatteryUsageStats */ false);
         synchronized (batteryStats) {
             batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
@@ -505,6 +506,102 @@
                 .of(180.0);
     }
 
+    @Test
+    public void accumulateBatteryUsageStats() {
+        BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+
+        setTime(5 * MINUTE_IN_MS);
+
+        // Capture the session start timestamp
+        synchronized (batteryStats) {
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+        }
+
+        PowerStatsStore powerStatsStore = new PowerStatsStore(
+                new File(mStatsRule.getHistoryDir(), getClass().getSimpleName()),
+                mStatsRule.getHandler());
+        powerStatsStore.reset();
+
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
+                mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
+                mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+
+        batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+                /* accumulateBatteryUsageStats */ true);
+
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOnLocked(APP_UID,
+                    10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+        }
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOffLocked(APP_UID,
+                    20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
+        }
+
+        synchronized (batteryStats) {
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+        }
+
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOnLocked(APP_UID,
+                    30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
+        }
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOffLocked(APP_UID,
+                    50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
+        }
+        setTime(55 * MINUTE_IN_MS);
+        synchronized (batteryStats) {
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+        }
+
+        // This section has not been saved yet, but should be added to the accumulated totals
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOnLocked(APP_UID,
+                    80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+        }
+        synchronized (batteryStats) {
+            batteryStats.noteFlashlightOffLocked(APP_UID,
+                    110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
+        }
+        setTime(115 * MINUTE_IN_MS);
+
+        // Await completion
+        ConditionVariable done = new ConditionVariable();
+        mStatsRule.getHandler().post(done::open);
+        done.block();
+
+        BatteryUsageStats stats = provider.getBatteryUsageStats(batteryStats,
+                new BatteryUsageStatsQuery.Builder().accumulated().build());
+
+        assertThat(stats.getStatsStartTimestamp()).isEqualTo(5 * MINUTE_IN_MS);
+        assertThat(stats.getStatsEndTimestamp()).isEqualTo(115 * MINUTE_IN_MS);
+
+        // Section 1 (saved): 20 - 10 = 10
+        // Section 2 (saved): 50 - 30 = 20
+        // Section 3 (fresh): 110 - 80 = 30
+        // Total: 10 + 20 + 30 = 60
+        assertThat(stats.getAggregateBatteryConsumer(
+                        BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+                .getConsumedPower(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+                .isWithin(0.0001)
+                .of(360.0);  // 360 mA * 1.0 hour
+        assertThat(stats.getAggregateBatteryConsumer(
+                        BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+                .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+                .isEqualTo(60 * MINUTE_IN_MS);
+
+        final UidBatteryConsumer uidBatteryConsumer = stats.getUidBatteryConsumers().stream()
+                .filter(uid -> uid.getUid() == APP_UID).findFirst().get();
+        assertThat(uidBatteryConsumer
+                .getConsumedPower(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+                .isWithin(0.1)
+                .of(360.0);
+        assertThat(uidBatteryConsumer
+                .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+                .isEqualTo(60 * MINUTE_IN_MS);
+    }
+
     private void setTime(long timeMs) {
         mMockClock.currentTime = timeMs;
         mMockClock.realtime = timeMs;
@@ -550,7 +647,8 @@
             return null;
         }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
 
-        mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+        mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+                /* accumulateBatteryUsageStats */ false);
 
         // Make an incompatible change of supported energy components.  This will trigger
         // a BatteryStats reset, which will generate a snapshot of battery stats.
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index bbe0755..ac1b7c6 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -54,6 +54,7 @@
         "services.flags",
         "services.net",
         "services.people",
+        "services.supervision",
         "services.usage",
         "service-permission.stubs.system_server",
         "guava",
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
new file mode 100644
index 0000000..6bd4279
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.supervision
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.server.LocalServices
+import com.android.server.pm.UserManagerInternal
+import com.google.common.truth.Truth.assertThat
+import androidx.test.platform.app.InstrumentationRegistry
+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
+
+/**
+ * Unit tests for {@link SupervisionService}.
+ * <p/>
+ * Run with <code>atest SupervisionServiceTest</code>.
+ */
+@RunWith(AndroidJUnit4::class)
+class SupervisionServiceTest {
+    companion object {
+        const val USER_ID = 100
+    }
+
+    private lateinit var service: SupervisionService
+
+    @Rule
+    @JvmField
+    val mocks: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var mockUserManagerInternal: UserManagerInternal
+
+    @Before
+    fun setup() {
+        val context = InstrumentationRegistry.getInstrumentation().context
+
+        LocalServices.removeServiceForTest(UserManagerInternal::class.java)
+        LocalServices.addService(UserManagerInternal::class.java, mockUserManagerInternal)
+
+        service = SupervisionService(context)
+    }
+
+    @Test
+    fun testSetSupervisionEnabledForUser() {
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+
+        service.setSupervisionEnabledForUser(USER_ID, true)
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+
+        service.setSupervisionEnabledForUser(USER_ID, false)
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+    }
+
+    @Test
+    fun testSetSupervisionLockscreenEnabledForUser() {
+        var userData = service.getUserDataLocked(USER_ID)
+        assertThat(userData.supervisionLockScreenEnabled).isFalse()
+        assertThat(userData.supervisionLockScreenOptions).isNull()
+
+        service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, Bundle())
+        userData = service.getUserDataLocked(USER_ID)
+        assertThat(userData.supervisionLockScreenEnabled).isTrue()
+        assertThat(userData.supervisionLockScreenOptions).isNotNull()
+
+        service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, false, null)
+        userData = service.getUserDataLocked(USER_ID)
+        assertThat(userData.supervisionLockScreenEnabled).isFalse()
+        assertThat(userData.supervisionLockScreenOptions).isNull()
+    }
+}
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index b5258df..60fa52f 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -402,4 +402,73 @@
         // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture).
         verify(mWindowManager, times(0)).updateViewLayout(any(), any());
     }
-}
\ No newline at end of file
+
+    @Test
+    public void testPinchDrag() {
+        float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10;
+
+        MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(actionDown);
+
+        MotionEvent pointerDown = new MotionEventBuilder(MotionEvent.ACTION_POINTER_DOWN,
+                SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f)
+                )
+                .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(45f)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(pointerDown);
+
+        // Simulate ACTION_MOVE event (both fingers moving apart).
+        MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f - offsetY)
+                )
+                .rawXCursorPosition(mWindowLayoutParams.x + 10f)
+                .rawYCursorPosition(mWindowLayoutParams.y + 10f)
+                .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(45f + offsetY)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(actionMove);
+
+        MotionEvent pointerUp = new MotionEventBuilder(MotionEvent.ACTION_POINTER_UP, SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f - offsetY)
+                )
+                .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(45f + offsetY)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(pointerUp);
+
+        MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_MOUSE)
+                .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+                        .x(40f)
+                        .y(40f - offsetY)
+                )
+                .classification(MotionEvent.CLASSIFICATION_PINCH)
+                .build();
+        mTouchpadDebugView.dispatchTouchEvent(actionUp);
+
+        // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture).
+        verify(mWindowManager, times(0)).updateViewLayout(any(), any());
+    }
+}