Merge "Set the "Allow diagonal scrolling" switch minimum height to 48dp" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 93f3119..16c7017 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11462,6 +11462,12 @@
     method @RequiresPermission(android.Manifest.permission.SET_LOW_POWER_STANDBY_PORTS) public void release();
   }
 
+  @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public final class PowerMonitorReadings {
+    method @FlaggedApi("android.permission.flags.fine_power_monitor_permission") public int getGranularity();
+    field @FlaggedApi("android.permission.flags.fine_power_monitor_permission") public static final int GRANULARITY_FINE = 1; // 0x1
+    field @FlaggedApi("android.permission.flags.fine_power_monitor_permission") public static final int GRANULARITY_UNSPECIFIED = 0; // 0x0
+  }
+
   @Deprecated public class PowerWhitelistManager {
     method @Deprecated @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public void addToWhitelist(@NonNull String);
     method @Deprecated @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public void addToWhitelist(@NonNull java.util.List<java.lang.String>);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index c2ce7d5..3c37b44 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6942,6 +6942,12 @@
         public RemoteViews makePublicContentView(boolean isLowPriority) {
             if (mN.publicVersion != null) {
                 final Builder builder = recoverBuilder(mContext, mN.publicVersion);
+                // copy non-sensitive style fields to the public style
+                if (mStyle instanceof Notification.MessagingStyle privateStyle) {
+                    if (builder.mStyle instanceof Notification.MessagingStyle publicStyle) {
+                        publicStyle.mConversationType = privateStyle.mConversationType;
+                    }
+                }
                 return builder.createContentView();
             }
             Bundle savedBundle = mN.extras;
diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig
index f750a84..be9e286 100644
--- a/core/java/android/app/wallpaper.aconfig
+++ b/core/java/android/app/wallpaper.aconfig
@@ -40,3 +40,10 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "enable_connected_displays_wallpaper"
+  namespace: "lse_desktop_experience"
+  description: "Enable wallpaper support in connected displays"
+  bug: "366461618"
+}
diff --git a/core/java/android/os/IPowerStatsService.aidl b/core/java/android/os/IPowerStatsService.aidl
index a0c2262..e0e9497 100644
--- a/core/java/android/os/IPowerStatsService.aidl
+++ b/core/java/android/os/IPowerStatsService.aidl
@@ -25,6 +25,8 @@
     const String KEY_ENERGY = "energy";
     /** @hide */
     const String KEY_TIMESTAMPS = "timestamps";
+    /** @hide */
+    const String KEY_GRANULARITY = "granularity";
 
     /** @hide */
     const int RESULT_SUCCESS = 0;
diff --git a/core/java/android/os/PowerMonitorReadings.java b/core/java/android/os/PowerMonitorReadings.java
index a0ab066..85ffc46 100644
--- a/core/java/android/os/PowerMonitorReadings.java
+++ b/core/java/android/os/PowerMonitorReadings.java
@@ -18,8 +18,12 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Arrays;
 import java.util.Comparator;
 
@@ -38,6 +42,37 @@
     @NonNull
     private final long[] mTimestampsMs;
 
+    /**
+     * PowerMonitorReadings have the default level of granularity, which may be coarse or fine
+     * as determined by the implementation.
+     * @hide
+     */
+    @FlaggedApi(android.permission.flags.Flags.FLAG_FINE_POWER_MONITOR_PERMISSION)
+    @SystemApi
+    public static final int GRANULARITY_UNSPECIFIED = 0;
+
+    /**
+     * PowerMonitorReadings have a high level of granularity. This level of granularity is
+     * provided to applications that have the
+     * {@link android.Manifest.permission#ACCESS_FINE_POWER_MONITORS} permission.
+     *
+     * @hide
+     */
+    @FlaggedApi(android.permission.flags.Flags.FLAG_FINE_POWER_MONITOR_PERMISSION)
+    @SystemApi
+    public static final int GRANULARITY_FINE = 1;
+
+    /** @hide */
+    @IntDef(prefix = {"GRANULARITY_"}, value = {
+            GRANULARITY_UNSPECIFIED,
+            GRANULARITY_FINE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PowerMonitorGranularity {}
+
+    @PowerMonitorGranularity
+    private final int mGranularity;
+
     private static final Comparator<PowerMonitor> POWER_MONITOR_COMPARATOR =
             Comparator.comparingInt(pm -> pm.index);
 
@@ -46,10 +81,12 @@
      * @hide
      */
     public PowerMonitorReadings(@NonNull PowerMonitor[] powerMonitors,
-            @NonNull long[] energyUws, @NonNull long[] timestampsMs) {
+            @NonNull long[] energyUws, @NonNull long[] timestampsMs,
+            @PowerMonitorGranularity int granularity) {
         mPowerMonitors = powerMonitors;
         mEnergyUws = energyUws;
         mTimestampsMs = timestampsMs;
+        mGranularity = granularity;
     }
 
     /**
@@ -79,6 +116,19 @@
         return 0;
     }
 
+    /**
+     * Returns the granularity level of the results, which refers to the maximum age of the
+     * power monitor readings, {@link #GRANULARITY_FINE} indicating the highest level
+     * of freshness supported by the service implementation.
+     * @hide
+     */
+    @FlaggedApi(android.permission.flags.Flags.FLAG_FINE_POWER_MONITOR_PERMISSION)
+    @SystemApi
+    @PowerMonitorGranularity
+    public int getGranularity() {
+        return mGranularity;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index a1e9cf2..febbfca 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -588,7 +588,8 @@
                     if (resultCode == IPowerStatsService.RESULT_SUCCESS) {
                         PowerMonitorReadings result = new PowerMonitorReadings(powerMonitorsArray,
                                 resultData.getLongArray(IPowerStatsService.KEY_ENERGY),
-                                resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS));
+                                resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS),
+                                resultData.getInt(IPowerStatsService.KEY_GRANULARITY));
                         if (executor != null) {
                             executor.execute(() -> onResult.onResult(result));
                         } else {
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index b38feee..f178b0e 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -167,3 +167,14 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "windowing_sdk"
+    name: "use_self_sync_transaction_for_layer"
+    description: "Always use this.getSyncTransaction for assignLayer"
+    bug: "388127825"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 06cd44e..9b3a6cb 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -111,7 +111,7 @@
     <shortcode country="cz" premium="90\\d{5}|90\\d{3}" free="116\\d{3}" />
 
     <!-- Germany: 4-5 digits plus 1232xxx (premium codes from http://www.vodafone.de/infofaxe/537.pdf and http://premiumdienste.eplus.de/pdf/kodex.pdf), plus EU. To keep the premium regex from being too large, it only includes payment processors that have been used by SMS malware, with the regular pattern matching the other premium short codes. -->
-    <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438" />
+    <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438|70997" />
 
     <!-- Denmark: see http://iprs.webspacecommerce.com/Denmark-Premium-Rate-Numbers -->
     <shortcode country="dk" pattern="\\d{4,5}" premium="1\\d{3}" free="116\\d{3}|4665" />
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index f136e06..a30570a 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -264,6 +264,7 @@
         <!-- Needed for test only -->
         <permission name="android.permission.BATTERY_PREDICTION"/>
         <permission name="android.permission.BATTERY_STATS"/>
+        <permission name="android.permission.ACCESS_FINE_POWER_MONITORS" />
         <!-- BLUETOOTH_PRIVILEGED is needed for test only -->
         <permission name="android.permission.BLUETOOTH_PRIVILEGED"/>
         <permission name="android.permission.BIND_APPWIDGET"/>
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index b2ac640..4f1cd97 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -26,6 +26,7 @@
     <uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
     <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" />
     <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" />
+    <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
 
     <application>
         <activity
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
index d15fbed..23498de 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
@@ -32,9 +32,7 @@
     /** Transitions with source unknown. */
     UNKNOWN;
 
-    override fun describeContents(): Int {
-        return 0
-    }
+    override fun describeContents(): Int = 0
 
     override fun writeToParcel(dest: Parcel, flags: Int) {
         dest.writeString(name)
@@ -44,9 +42,8 @@
         @JvmField
         val CREATOR =
             object : Parcelable.Creator<DesktopModeTransitionSource> {
-                override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource {
-                    return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
-                }
+                override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource =
+                    parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
 
                 override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size)
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
index 82ef00e..8cd7b0f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.appzoomout;
 
-import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.app.ActivityManager;
@@ -101,7 +100,6 @@
 
         mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
         mDisplayController.addDisplayChangingController(this);
-        updateDisplayLayout(mContext.getDisplayId());
 
         mDisplayAreaOrganizer.registerOrganizer();
     }
@@ -137,9 +135,7 @@
     public void onDisplayChange(int displayId, int fromRotation, int toRotation,
             @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
         // TODO: verify if there is synchronization issues.
-        if (toRotation != ROTATION_UNDEFINED) {
-            mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation);
-        }
+        mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index f532be6..72be066 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -21,6 +21,7 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayTopology;
 import android.os.RemoteException;
 import android.util.ArraySet;
 import android.util.Size;
@@ -34,6 +35,7 @@
 
 import androidx.annotation.BinderThread;
 
+import com.android.window.flags.Flags;
 import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
@@ -54,6 +56,7 @@
     private final ShellExecutor mMainExecutor;
     private final Context mContext;
     private final IWindowManager mWmService;
+    private final DisplayManager mDisplayManager;
     private final DisplayChangeController mChangeController;
     private final IDisplayWindowListener mDisplayContainerListener;
 
@@ -61,10 +64,11 @@
     private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
 
     public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit,
-            ShellExecutor mainExecutor) {
+            ShellExecutor mainExecutor, DisplayManager displayManager) {
         mMainExecutor = mainExecutor;
         mContext = context;
         mWmService = wmService;
+        mDisplayManager = displayManager;
         // TODO: Inject this instead
         mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor);
         mDisplayContainerListener = new DisplayWindowListenerImpl();
@@ -74,7 +78,7 @@
     }
 
     /**
-     * Initializes the window listener.
+     * Initializes the window listener and the topology listener.
      */
     public void onInit() {
         try {
@@ -82,6 +86,12 @@
             for (int i = 0; i < displayIds.length; i++) {
                 onDisplayAdded(displayIds[i]);
             }
+
+            if (Flags.enableConnectedDisplaysWindowDrag()) {
+                mDisplayManager.registerTopologyListener(mMainExecutor,
+                        this::onDisplayTopologyChanged);
+                onDisplayTopologyChanged(mDisplayManager.getDisplayTopology());
+            }
         } catch (RemoteException e) {
             throw new RuntimeException("Unable to register display controller");
         }
@@ -91,8 +101,7 @@
      * Gets a display by id from DisplayManager.
      */
     public Display getDisplay(int displayId) {
-        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
-        return displayManager.getDisplay(displayId);
+        return mDisplayManager.getDisplay(displayId);
     }
 
     /**
@@ -221,6 +230,14 @@
         }
     }
 
+    private void onDisplayTopologyChanged(DisplayTopology topology) {
+        // TODO(b/381472611): Call DisplayTopology#getCoordinates and update values in
+        //                    DisplayLayout when DM code is ready.
+        for (int i = 0; i < mDisplayChangedListeners.size(); ++i) {
+            mDisplayChangedListeners.get(i).onTopologyChanged();
+        }
+    }
+
     private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
         synchronized (mDisplays) {
             final DisplayRecord dr = mDisplays.get(displayId);
@@ -408,5 +425,10 @@
          */
         default void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
                 Set<Rect> unrestricted) {}
+
+        /**
+         * Called when the display topology has changed.
+         */
+        default void onTopologyChanged() {}
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index b6a1686..4973a6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -31,7 +31,9 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Insets;
+import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.SystemProperties;
 import android.provider.Settings;
 import android.util.DisplayMetrics;
@@ -71,9 +73,12 @@
     public static final int NAV_BAR_RIGHT = 1 << 1;
     public static final int NAV_BAR_BOTTOM = 1 << 2;
 
+    private static final String TAG = "DisplayLayout";
+
     private int mUiMode;
     private int mWidth;
     private int mHeight;
+    private RectF mGlobalBoundsDp;
     private DisplayCutout mCutout;
     private int mRotation;
     private int mDensityDpi;
@@ -109,6 +114,7 @@
         return mUiMode == other.mUiMode
                 && mWidth == other.mWidth
                 && mHeight == other.mHeight
+                && Objects.equals(mGlobalBoundsDp, other.mGlobalBoundsDp)
                 && Objects.equals(mCutout, other.mCutout)
                 && mRotation == other.mRotation
                 && mDensityDpi == other.mDensityDpi
@@ -127,8 +133,8 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi,
-                mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
+        return Objects.hash(mUiMode, mWidth, mHeight, mGlobalBoundsDp, mCutout, mRotation,
+                mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
                 mNavBarFrameHeight, mTaskbarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving,
                 mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState);
     }
@@ -170,6 +176,7 @@
         mUiMode = dl.mUiMode;
         mWidth = dl.mWidth;
         mHeight = dl.mHeight;
+        mGlobalBoundsDp = dl.mGlobalBoundsDp;
         mCutout = dl.mCutout;
         mRotation = dl.mRotation;
         mDensityDpi = dl.mDensityDpi;
@@ -193,6 +200,7 @@
         mRotation = info.rotation;
         mCutout = info.displayCutout;
         mDensityDpi = info.logicalDensityDpi;
+        mGlobalBoundsDp = new RectF(0, 0, pxToDp(mWidth), pxToDp(mHeight));
         mHasNavigationBar = hasNavigationBar;
         mHasStatusBar = hasStatusBar;
         mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean(
@@ -255,6 +263,11 @@
         recalcInsets(res);
     }
 
+    /** Update the global bounds of this layout, in DP. */
+    public void setGlobalBoundsDp(RectF bounds) {
+        mGlobalBoundsDp = bounds;
+    }
+
     /** Get this layout's non-decor insets. */
     public Rect nonDecorInsets() {
         return mNonDecorInsets;
@@ -265,16 +278,21 @@
         return mStableInsets;
     }
 
-    /** Get this layout's width. */
+    /** Get this layout's width in pixels. */
     public int width() {
         return mWidth;
     }
 
-    /** Get this layout's height. */
+    /** Get this layout's height in pixels. */
     public int height() {
         return mHeight;
     }
 
+    /** Get this layout's global bounds in the multi-display coordinate system in DP. */
+    public RectF globalBoundsDp() {
+        return mGlobalBoundsDp;
+    }
+
     /** Get this layout's display rotation. */
     public int rotation() {
         return mRotation;
@@ -486,4 +504,48 @@
                 ? R.dimen.navigation_bar_frame_height_landscape
                 : R.dimen.navigation_bar_frame_height);
     }
+
+    /**
+     * Converts a pixel value to a density-independent pixel (dp) value.
+     *
+     * @param px The pixel value to convert.
+     * @return The equivalent value in DP units.
+     */
+    public float pxToDp(Number px) {
+        return px.floatValue() * DisplayMetrics.DENSITY_DEFAULT / mDensityDpi;
+    }
+
+    /**
+     * Converts a density-independent pixel (dp) value to a pixel value.
+     *
+     * @param dp The DP value to convert.
+     * @return The equivalent value in pixel units.
+     */
+    public float dpToPx(Number dp) {
+        return dp.floatValue() * mDensityDpi / DisplayMetrics.DENSITY_DEFAULT;
+    }
+
+    /**
+     * Converts local pixel coordinates on this layout to global DP coordinates.
+     *
+     * @param xPx The x-coordinate in pixels, relative to the layout's origin.
+     * @param yPx The y-coordinate in pixels, relative to the layout's origin.
+     * @return A PointF object representing the coordinates in global DP units.
+     */
+    public PointF localPxToGlobalDp(Number xPx, Number yPx) {
+        return new PointF(mGlobalBoundsDp.left + pxToDp(xPx),
+                mGlobalBoundsDp.top + pxToDp(yPx));
+    }
+
+    /**
+     * Converts global DP coordinates to local pixel coordinates on this layout.
+     *
+     * @param xDp The x-coordinate in global DP units.
+     * @param yDp The y-coordinate in global DP units.
+     * @return A PointF object representing the coordinates in local pixel units on this layout.
+     */
+    public PointF globalDpToLocalPx(Number xDp, Number yDp) {
+        return new PointF(dpToPx(xDp.floatValue() - mGlobalBoundsDp.left),
+                dpToPx(yDp.floatValue() - mGlobalBoundsDp.top));
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index ab3c33e..cbbe8a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -25,6 +25,7 @@
 import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.hardware.display.DisplayManager;
 import android.os.Handler;
 import android.os.SystemProperties;
 import android.provider.Settings;
@@ -175,8 +176,9 @@
     static DisplayController provideDisplayController(Context context,
             IWindowManager wmService,
             ShellInit shellInit,
-            @ShellMainThread ShellExecutor mainExecutor) {
-        return new DisplayController(context, wmService, shellInit, mainExecutor);
+            @ShellMainThread ShellExecutor mainExecutor,
+            DisplayManager displayManager) {
+        return new DisplayController(context, wmService, shellInit, mainExecutor, displayManager);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
index ca02c72..f6fd967 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
@@ -93,9 +93,8 @@
         return matchingChanges.first()
     }
 
-    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean {
-        return change.taskInfo != null && change.taskInfo?.taskId != -1
-    }
+    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean =
+        change.taskInfo != null && change.taskInfo?.taskId != -1
 
     override fun handleRequest(
         transition: IBinder,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index e975b58..c09504e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -434,18 +434,14 @@
         visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo)
     }
 
-    private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo {
-        return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
-    }
+    private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo =
+        this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
 
-    private fun TaskInfo.isFreeformWindow(): Boolean {
-        return this.windowingMode == WINDOWING_MODE_FREEFORM
-    }
+    private fun TaskInfo.isFreeformWindow(): Boolean = this.windowingMode == WINDOWING_MODE_FREEFORM
 
-    private fun TransitionInfo.isExitToRecentsTransition(): Boolean {
-        return this.type == WindowManager.TRANSIT_TO_FRONT &&
+    private fun TransitionInfo.isExitToRecentsTransition(): Boolean =
+        this.type == WindowManager.TRANSIT_TO_FRONT &&
             this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
-    }
 
     companion object {
         @VisibleForTesting const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
index dba8c93..cdfa14b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -24,8 +24,8 @@
 class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) :
     ShellCommandHandler.ShellCommandActionHandler {
 
-    override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean {
-        return when (args[0]) {
+    override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean =
+        when (args[0]) {
             "moveToDesktop" -> {
                 if (!runMoveToDesktop(args, pw)) {
                     pw.println("Task not found. Please enter a valid taskId.")
@@ -47,7 +47,6 @@
                 false
             }
         }
-    }
 
     private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean {
         if (args.size < 2) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 1a58363..c975533 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -470,7 +470,7 @@
      * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id
      * will be looked up from the task id.
      */
-    fun removeFreeformTask(displayId: Int, taskId: Int) {
+    fun removeTask(displayId: Int, taskId: Int) {
         logD("Removes freeform task: taskId=%d", taskId)
         if (displayId == INVALID_DISPLAY) {
             // Removes the original display id of the task.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
index 947a8dd..c958a09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
@@ -29,7 +29,7 @@
         val desktopRepository: DesktopRepository =
             desktopUserRepositories.getProfile(taskInfo.userId)
         if (!isFreeformTask(taskInfo) && desktopRepository.isActiveTask(taskInfo.taskId)) {
-            desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId)
+            desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId)
             return
         }
         if (isFreeformTask(taskInfo)) {
@@ -50,7 +50,7 @@
             desktopRepository.updateTask(taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible)
         } else {
             // Case 2: Freeform task is changed outside Desktop Mode.
-            desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId)
+            desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId)
         }
     }
 
@@ -60,7 +60,7 @@
     // Any changes to [DesktopRepository] from this method should be made carefully to minimize risk
     // of race conditions and possible duplications with [onTaskChanging].
     override fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo) {
-        // TODO: b/367268953 - Propapagate usages from FreeformTaskListener to this method.
+        // TODO: b/367268953 - Propagate usages from FreeformTaskListener to this method.
     }
 
     override fun onTaskMovingToFront(taskInfo: RunningTaskInfo) {
@@ -68,7 +68,7 @@
             desktopUserRepositories.getProfile(taskInfo.userId)
         if (!desktopRepository.isActiveTask(taskInfo.taskId)) return
         if (!isFreeformTask(taskInfo)) {
-            desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId)
+            desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId)
         }
         // TODO: b/367268953 - Connect this with DesktopRepository for handling
         // task moving to front for tasks in windowing mode.
@@ -90,7 +90,7 @@
             // A task that's vanishing should be removed:
             // - If it's closed by the X button which means it's marked as a closing task.
             desktopRepository.removeClosingTask(taskInfo.taskId)
-            desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId)
+            desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId)
         } else {
             desktopRepository.updateTask(taskInfo.displayId, taskInfo.taskId, isVisible = false)
             desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
index 848d80f..f29301d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
@@ -41,49 +41,35 @@
             return Point(x, y.toInt())
         }
 
-        override fun next(): DesktopTaskPosition {
-            return BottomRight
-        }
+        override fun next(): DesktopTaskPosition = BottomRight
     }
 
     data object BottomRight : DesktopTaskPosition() {
-        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
-            return Point(frame.right - window.width(), frame.bottom - window.height())
-        }
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+            Point(frame.right - window.width(), frame.bottom - window.height())
 
-        override fun next(): DesktopTaskPosition {
-            return TopLeft
-        }
+        override fun next(): DesktopTaskPosition = TopLeft
     }
 
     data object TopLeft : DesktopTaskPosition() {
-        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
-            return Point(frame.left, frame.top)
-        }
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+            Point(frame.left, frame.top)
 
-        override fun next(): DesktopTaskPosition {
-            return BottomLeft
-        }
+        override fun next(): DesktopTaskPosition = BottomLeft
     }
 
     data object BottomLeft : DesktopTaskPosition() {
-        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
-            return Point(frame.left, frame.bottom - window.height())
-        }
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+            Point(frame.left, frame.bottom - window.height())
 
-        override fun next(): DesktopTaskPosition {
-            return TopRight
-        }
+        override fun next(): DesktopTaskPosition = TopRight
     }
 
     data object TopRight : DesktopTaskPosition() {
-        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
-            return Point(frame.right - window.width(), frame.top)
-        }
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+            Point(frame.right - window.width(), frame.top)
 
-        override fun next(): DesktopTaskPosition {
-            return Center
-        }
+        override fun next(): DesktopTaskPosition = Center
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 6013648..73d1527 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -1470,13 +1470,9 @@
         }
     }
 
-    override fun getContext(): Context {
-        return context
-    }
+    override fun getContext(): Context = context
 
-    override fun getRemoteCallExecutor(): ShellExecutor {
-        return mainExecutor
-    }
+    override fun getRemoteCallExecutor(): ShellExecutor = mainExecutor
 
     override fun startAnimation(
         transition: IBinder,
@@ -1662,11 +1658,10 @@
         DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() &&
             isTopActivityExemptFromDesktopWindowing(context, task)
 
-    private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean {
-        return ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
+    private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean =
+        ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
             TransitionUtil.isClosingType(request.type) &&
             request.triggerTask != null
-    }
 
     /** Open an existing instance of an app. */
     fun openInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) {
@@ -2185,11 +2180,10 @@
         getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) }
     }
 
-    private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? {
-        return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
+    private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? =
+        shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
             taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
         }
-    }
 
     /**
      * Requests a task be transitioned from desktop to split select. Applies needed windowing
@@ -2237,14 +2231,10 @@
         }
     }
 
-    private fun getDefaultDensityDpi(): Int {
-        return context.resources.displayMetrics.densityDpi
-    }
+    private fun getDefaultDensityDpi(): Int = context.resources.displayMetrics.densityDpi
 
     /** Creates a new instance of the external interface to pass to another process. */
-    private fun createExternalInterface(): ExternalInterfaceBinder {
-        return IDesktopModeImpl(this)
-    }
+    private fun createExternalInterface(): ExternalInterfaceBinder = IDesktopModeImpl(this)
 
     /** Get connection interface between sysui and shell */
     fun asDesktopMode(): DesktopMode {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index c2dd4d28..e4a28e9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -138,11 +138,10 @@
             )
         }
 
-        private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? {
-            return info.changes.find { change ->
+        private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
+            info.changes.find { change ->
                 change.taskInfo?.taskId == taskId && change.mode == TRANSIT_TO_BACK
             }
-        }
 
         override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
             if (activeTransitionTokensAndTasks.remove(merged) != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index d61ffda..b9a65fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -141,7 +141,7 @@
                 desktopRepository.isActiveTask(taskInfo.taskId) &&
                     taskInfo.windowingMode != WINDOWING_MODE_FREEFORM
             ) {
-                desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId)
+                desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId)
             }
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
index 7f3133e..13576aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
@@ -34,6 +34,7 @@
 import com.android.wm.shell.sysui.UserChangeListener
 import java.io.PrintWriter
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 /** Manages per-user DesktopRepository instances. */
 class DesktopUserRepositories(
@@ -43,7 +44,7 @@
     private val persistentRepository: DesktopPersistentRepository,
     private val repositoryInitializer: DesktopRepositoryInitializer,
     @ShellMainThread private val mainCoroutineScope: CoroutineScope,
-    userManager: UserManager,
+    private val userManager: UserManager,
 ) : UserChangeListener {
     private var userId: Int
     private var userIdToProfileIdsMap: MutableMap<Int, List<Int>> = mutableMapOf()
@@ -100,6 +101,9 @@
     override fun onUserChanged(newUserId: Int, userContext: Context) {
         logD("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId)
         userId = newUserId
+        if (Flags.enableDesktopWindowingHsum()) {
+            sanitizeUsers()
+        }
     }
 
     override fun onUserProfilesChanged(profiles: MutableList<UserInfo>) {
@@ -110,10 +114,34 @@
         }
     }
 
+    private fun sanitizeUsers() {
+        val aliveUserIds = userManager.getAliveUsers().map { it.id }
+        val usersToDelete = userIdToProfileIdsMap.keys.filterNot { it in aliveUserIds }
+
+        usersToDelete.forEach { uid ->
+            userIdToProfileIdsMap.remove(uid)
+            desktopRepoByUserId.remove(uid)
+        }
+        mainCoroutineScope.launch {
+            try {
+                persistentRepository.removeUsers(usersToDelete)
+            } catch (exception: Exception) {
+                logE(
+                    "An exception occurred while updating the persistent repository \n%s",
+                    exception.stackTrace,
+                )
+            }
+        }
+    }
+
     private fun logD(msg: String, vararg arguments: Any?) {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
     }
 
+    private fun logE(msg: String, vararg arguments: Any?) {
+        ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+
     companion object {
         private const val TAG = "DesktopUserRepositories"
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index 1380a9c..91f10dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -765,9 +765,8 @@
         transitionState = null
     }
 
-    private fun isSplitTask(taskId: Int): Boolean {
-        return splitScreenController.isTaskInSplitScreen(taskId)
-    }
+    private fun isSplitTask(taskId: Int): Boolean =
+        splitScreenController.isTaskInSplitScreen(taskId)
 
     private fun getOtherSplitTask(taskId: Int): Int? {
         val splitPos = splitScreenController.getSplitPosition(taskId)
@@ -781,9 +780,8 @@
         return splitScreenController.getTaskInfo(otherTaskPos)?.taskId
     }
 
-    protected fun requireTransitionState(): TransitionState {
-        return transitionState ?: error("Expected non-null transition state")
-    }
+    protected fun requireTransitionState(): TransitionState =
+        transitionState ?: error("Expected non-null transition state")
 
     /**
      * Represents the layering (Z order) that will be given to any window based on its type during
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index a47e937..5e84019 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -156,13 +156,11 @@
         return matchingChanges.first()
     }
 
-    private fun isWallpaper(change: TransitionInfo.Change): Boolean {
-        return (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
-    }
+    private fun isWallpaper(change: TransitionInfo.Change): Boolean =
+        (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
 
-    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean {
-        return change.taskInfo != null && change.taskInfo?.taskId != -1
-    }
+    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean =
+        change.taskInfo != null && change.taskInfo?.taskId != -1
 
     companion object {
         private const val RESIZE_DURATION_MS = 300L
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
index a6998e1..9e41270 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
@@ -141,6 +141,22 @@
         }
     }
 
+    suspend fun removeUsers(uids: List<Int>) {
+        try {
+            dataStore.updateData { persistentRepositories: DesktopPersistentRepositories ->
+                val persistentRepositoriesBuilder = persistentRepositories.toBuilder()
+                uids.forEach { uid -> persistentRepositoriesBuilder.removeDesktopRepoByUser(uid) }
+                persistentRepositoriesBuilder.build()
+            }
+        } catch (exception: Exception) {
+            Log.e(
+                TAG,
+                "Error in removing user related data, data is stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE",
+                exception,
+            )
+        }
+    }
+
     private fun getDesktop(currentRepository: DesktopRepositoryState, desktopId: Int): Desktop =
         // If there are no desktops set up, create one on the default display
         currentRepository.getDesktopOrDefault(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 4b59efb..0d5aa01 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -127,7 +127,7 @@
                 // triggered on a task and the task is closing. It will be marked as minimized in
                 // [DesktopTasksTransitionObserver] before it gets here.
                 repository.removeClosingTask(taskInfo.taskId);
-                repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
+                repository.removeTask(taskInfo.displayId, taskInfo.taskId);
             }
         }
         mWindowDecorationViewModel.onTaskVanished(taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 0869caa..d6f9183 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -299,7 +299,7 @@
     @Override
     public void onRecentTaskRemovedForAddTask(int taskId) {
         mDesktopUserRepositories.ifPresent(
-                desktopUserRepositories -> desktopUserRepositories.getCurrent().removeFreeformTask(
+                desktopUserRepositories -> desktopUserRepositories.getCurrent().removeTask(
                         INVALID_DISPLAY, taskId));
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
index 1e5e153..d3de0f7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
+import android.hardware.display.DisplayManager;
 import android.view.IWindowManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -50,12 +51,14 @@
     private @Mock IWindowManager mWM;
     private @Mock ShellInit mShellInit;
     private @Mock ShellExecutor mMainExecutor;
+    private @Mock DisplayManager mDisplayManager;
     private DisplayController mController;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor);
+        mController = new DisplayController(
+                mContext, mWM, mShellInit, mMainExecutor, mDisplayManager);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
index d467b39..b0a455d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
@@ -33,7 +33,9 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Insets;
+import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
 
@@ -58,6 +60,7 @@
 @SmallTest
 public class DisplayLayoutTest extends ShellTestCase {
     private MockitoSession mMockitoSession;
+    private static final float DELTA = 0.1f; // Constant for assertion delta
 
     @Before
     public void setup() {
@@ -130,6 +133,39 @@
         assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets());
     }
 
+    @Test
+    public void testDpPxConversion() {
+        int px = 100;
+        float dp = 53.33f;
+        int xPx = 100;
+        int yPx = 200;
+        float xDp = 164.33f;
+        float yDp = 328.66f;
+
+        Resources res = createResources(40, 50, false);
+        DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0);
+        DisplayLayout dl = new DisplayLayout(info, res, false, false);
+        dl.setGlobalBoundsDp(new RectF(111f, 222f, 300f, 400f));
+
+        // Test pxToDp
+        float resultDp = dl.pxToDp(px);
+        assertEquals(dp, resultDp, DELTA);
+
+        // Test dpToPx
+        float resultPx = dl.dpToPx(dp);
+        assertEquals(px, resultPx, DELTA);
+
+        // Test localPxToGlobalDp
+        PointF resultGlobalDp = dl.localPxToGlobalDp(xPx, yPx);
+        assertEquals(xDp, resultGlobalDp.x, DELTA);
+        assertEquals(yDp, resultGlobalDp.y, DELTA);
+
+        // Test globalDpToLocalPx
+        PointF resultLocalPx = dl.globalDpToLocalPx(xDp, yDp);
+        assertEquals(xPx, resultLocalPx.x, DELTA);
+        assertEquals(yPx, resultLocalPx.y, DELTA);
+    }
+
     private Resources createResources(int navLand, int navPort, boolean navMoves) {
         Configuration cfg = new Configuration();
         cfg.uiMode = UI_MODE_TYPE_NORMAL;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index 9b24c1c..eb6f1d7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -501,13 +501,12 @@
         }
     }
 
-    private fun createTaskInfo(): RunningTaskInfo {
-        return TestRunningTaskInfoBuilder()
+    private fun createTaskInfo(): RunningTaskInfo =
+        TestRunningTaskInfoBuilder()
             .setTaskId(TASK_ID)
             .setUid(TASK_UID)
             .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT))
             .build()
-    }
 
     private fun verifyNoLogging() {
         verify(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index daeccce..6003a21 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -669,10 +669,10 @@
     }
 
     @Test
-    fun removeFreeformTask_invalidDisplay_removesTaskFromFreeformTasks() {
+    fun removeTask_invalidDisplay_removesTaskFromFreeformTasks() {
         repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
 
-        repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1)
+        repo.removeTask(INVALID_DISPLAY, taskId = 1)
 
         val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY)
         assertThat(invalidDisplayTasks).isEmpty()
@@ -682,11 +682,11 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
-    fun removeFreeformTask_invalidDisplay_persistenceEnabled_removesTaskFromFreeformTasks() {
+    fun removeTask_invalidDisplay_persistenceEnabled_removesTaskFromFreeformTasks() {
         runTest(StandardTestDispatcher()) {
             repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
 
-            repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1)
+            repo.removeTask(INVALID_DISPLAY, taskId = 1)
 
             verify(persistentRepository)
                 .addOrUpdateDesktop(
@@ -708,10 +708,10 @@
     }
 
     @Test
-    fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() {
+    fun removeTask_validDisplay_removesTaskFromFreeformTasks() {
         repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
 
-        repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1)
+        repo.removeTask(DEFAULT_DISPLAY, taskId = 1)
 
         val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
         assertThat(tasks).isEmpty()
@@ -719,11 +719,11 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
-    fun removeFreeformTask_validDisplay_persistenceEnabled_removesTaskFromFreeformTasks() {
+    fun removeTask_validDisplay_persistenceEnabled_removesTaskFromFreeformTasks() {
         runTest(StandardTestDispatcher()) {
             repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
 
-            repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1)
+            repo.removeTask(DEFAULT_DISPLAY, taskId = 1)
 
             verify(persistentRepository)
                 .addOrUpdateDesktop(
@@ -745,10 +745,10 @@
     }
 
     @Test
-    fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() {
+    fun removeTask_validDisplay_differentDisplay_doesNotRemovesTask() {
         repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
 
-        repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1)
+        repo.removeTask(SECOND_DISPLAY, taskId = 1)
 
         val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
         assertThat(tasks).containsExactly(1)
@@ -756,11 +756,11 @@
 
     @Test
     @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
-    fun removeFreeformTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() {
+    fun removeTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() {
         runTest(StandardTestDispatcher()) {
             repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
 
-            repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1)
+            repo.removeTask(SECOND_DISPLAY, taskId = 1)
 
             verify(persistentRepository)
                 .addOrUpdateDesktop(
@@ -782,57 +782,57 @@
     }
 
     @Test
-    fun removeFreeformTask_removesTaskBoundsBeforeMaximize() {
+    fun removeTask_removesTaskBoundsBeforeMaximize() {
         val taskId = 1
         repo.addTask(THIRD_DISPLAY, taskId, isVisible = true)
         repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
 
-        repo.removeFreeformTask(THIRD_DISPLAY, taskId)
+        repo.removeTask(THIRD_DISPLAY, taskId)
 
         assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull()
     }
 
     @Test
-    fun removeFreeformTask_removesTaskBoundsBeforeImmersive() {
+    fun removeTask_removesTaskBoundsBeforeImmersive() {
         val taskId = 1
         repo.addTask(THIRD_DISPLAY, taskId, isVisible = true)
         repo.saveBoundsBeforeFullImmersive(taskId, Rect(0, 0, 200, 200))
 
-        repo.removeFreeformTask(THIRD_DISPLAY, taskId)
+        repo.removeTask(THIRD_DISPLAY, taskId)
 
         assertThat(repo.removeBoundsBeforeFullImmersive(taskId)).isNull()
     }
 
     @Test
-    fun removeFreeformTask_removesActiveTask() {
+    fun removeTask_removesActiveTask() {
         val taskId = 1
         val listener = TestListener()
         repo.addActiveTaskListener(listener)
         repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
 
-        repo.removeFreeformTask(THIRD_DISPLAY, taskId)
+        repo.removeTask(THIRD_DISPLAY, taskId)
 
         assertThat(repo.isActiveTask(taskId)).isFalse()
         assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2)
     }
 
     @Test
-    fun removeFreeformTask_unminimizesTask() {
+    fun removeTask_unminimizesTask() {
         val taskId = 1
         repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
         repo.minimizeTask(DEFAULT_DISPLAY, taskId)
 
-        repo.removeFreeformTask(DEFAULT_DISPLAY, taskId)
+        repo.removeTask(DEFAULT_DISPLAY, taskId)
 
         assertThat(repo.isMinimizedTask(taskId)).isFalse()
     }
 
     @Test
-    fun removeFreeformTask_updatesTaskVisibility() {
+    fun removeTask_updatesTaskVisibility() {
         val taskId = 1
         repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
 
-        repo.removeFreeformTask(THIRD_DISPLAY, taskId)
+        repo.removeTask(THIRD_DISPLAY, taskId)
 
         assertThat(repo.isVisibleTask(taskId)).isFalse()
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt
index 19ab911..c7c0dfc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt
@@ -68,8 +68,7 @@
 
         verify(desktopUserRepositories.current, never())
             .addTask(task.displayId, task.taskId, task.isVisible)
-        verify(desktopUserRepositories.current, never())
-            .removeFreeformTask(task.displayId, task.taskId)
+        verify(desktopUserRepositories.current, never()).removeTask(task.displayId, task.taskId)
     }
 
     @Test
@@ -79,7 +78,7 @@
 
         desktopTaskChangeListener.onTaskOpening(task)
 
-        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+        verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId)
     }
 
     @Test
@@ -109,7 +108,7 @@
 
         desktopTaskChangeListener.onTaskChanging(task)
 
-        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+        verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId)
     }
 
     @Test
@@ -141,7 +140,7 @@
 
         desktopTaskChangeListener.onTaskMovingToFront(task)
 
-        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+        verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId)
     }
 
     @Test
@@ -169,7 +168,7 @@
 
         verify(desktopUserRepositories.current, never()).minimizeTask(task.displayId, task.taskId)
         verify(desktopUserRepositories.current).removeClosingTask(task.taskId)
-        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+        verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId)
     }
 
     @Test
@@ -182,6 +181,6 @@
         desktopTaskChangeListener.onTaskClosing(task)
 
         verify(desktopUserRepositories.current).removeClosingTask(task.taskId)
-        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+        verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId)
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 4bb7430..95ed8b4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -353,8 +353,8 @@
         taskRepository = userRepositories.current
     }
 
-    private fun createController(): DesktopTasksController {
-        return DesktopTasksController(
+    private fun createController() =
+        DesktopTasksController(
             context,
             shellInit,
             shellCommandHandler,
@@ -388,7 +388,6 @@
             desktopWallpaperActivityTokenProvider,
             Optional.of(bubbleController),
         )
-    }
 
     @After
     fun tearDown() {
@@ -4958,13 +4957,12 @@
         return task
     }
 
-    private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
+    private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo =
         // active = false marks the task as non-visible; PiP window doesn't count as visible tasks
-        return setUpFreeformTask(active = false).apply {
+        setUpFreeformTask(active = false).apply {
             pictureInPictureParams =
                 PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build()
         }
-    }
 
     private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
         val task = createHomeTask(displayId)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index 96ed214..ca1e3ed 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -223,7 +223,7 @@
         )
 
         verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
-        verify(taskRepository).removeFreeformTask(task.displayId, task.taskId)
+        verify(taskRepository).removeTask(task.displayId, task.taskId)
     }
 
     @Test
@@ -241,7 +241,7 @@
         )
 
         verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
-        verify(taskRepository).removeFreeformTask(task.displayId, task.taskId)
+        verify(taskRepository).removeTask(task.displayId, task.taskId)
     }
 
     @Test
@@ -458,8 +458,8 @@
         }
     }
 
-    private fun createCloseTransition(task: RunningTaskInfo?): TransitionInfo {
-        return TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply {
+    private fun createCloseTransition(task: RunningTaskInfo?) =
+        TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply {
             addChange(
                 Change(mock(), mock()).apply {
                     mode = TRANSIT_CLOSE
@@ -469,10 +469,9 @@
                 }
             )
         }
-    }
 
-    private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo {
-        return TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply {
+    private fun createToBackTransition(task: RunningTaskInfo?) =
+        TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply {
             addChange(
                 Change(mock(), mock()).apply {
                     mode = TRANSIT_TO_BACK
@@ -482,7 +481,6 @@
                 }
             )
         }
-    }
 
     private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo {
         return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 341df02..bf9cf00 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -676,8 +676,8 @@
             }
     }
 
-    private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo {
-        return TransitionInfo(type, /* flags= */ 0).apply {
+    private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) =
+        TransitionInfo(type, /* flags= */ 0).apply {
             addChange( // Home.
                 TransitionInfo.Change(mock(), homeTaskLeash).apply {
                     parent = null
@@ -700,7 +700,6 @@
                 }
             )
         }
-    }
 
     private fun systemPropertiesKey(name: String) =
         "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
index eae2066..5f92326 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
@@ -85,11 +85,11 @@
             val desk = createDesktop(task)
             val repositoryState =
                 DesktopRepositoryState.newBuilder().putDesktop(DEFAULT_DESKTOP_ID, desk)
-            val DesktopPersistentRepositories =
+            val desktopPersistentRepositories =
                 DesktopPersistentRepositories.newBuilder()
                     .putDesktopRepoByUser(DEFAULT_USER_ID, repositoryState.build())
                     .build()
-            testDatastore.updateData { DesktopPersistentRepositories }
+            testDatastore.updateData { desktopPersistentRepositories }
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
 
@@ -102,8 +102,8 @@
         runTest(StandardTestDispatcher()) {
             // Create a basic repository state
             val task = createDesktopTask(1)
-            val DesktopPersistentRepositories = createRepositoryWithOneDesk(task)
-            testDatastore.updateData { DesktopPersistentRepositories }
+            val desktopPersistentRepositories = createRepositoryWithOneDesk(task)
+            testDatastore.updateData { desktopPersistentRepositories }
             // Create a new state to be initialized
             val visibleTasks = ArraySet(listOf(1, 2))
             val minimizedTasks = ArraySet<Int>()
@@ -124,11 +124,46 @@
     }
 
     @Test
+    fun removeUsers_removesUsersData() {
+        runTest(StandardTestDispatcher()) {
+            val task = createDesktopTask(1)
+            val desktopPersistentRepositories = createRepositoryWithOneDesk(task)
+            testDatastore.updateData { desktopPersistentRepositories }
+            // Create a new state to be initialized
+            val visibleTasks = ArraySet(listOf(1))
+            val minimizedTasks = ArraySet(listOf(1))
+            val freeformTasksInZOrder = ArrayList(listOf(1))
+            datastoreRepository.addOrUpdateDesktop(
+                visibleTasks = visibleTasks,
+                minimizedTasks = minimizedTasks,
+                freeformTasksInZOrder = freeformTasksInZOrder,
+                userId = DEFAULT_USER_ID,
+            )
+            datastoreRepository.addOrUpdateDesktop(
+                visibleTasks = visibleTasks,
+                minimizedTasks = minimizedTasks,
+                freeformTasksInZOrder = freeformTasksInZOrder,
+                userId = USER_ID_2,
+            )
+
+            datastoreRepository.removeUsers(mutableListOf(USER_ID_2))
+
+            val removedDesktopRepositoryState =
+                datastoreRepository.getDesktopRepositoryState(USER_ID_2)
+            assertThat(removedDesktopRepositoryState).isEqualTo(null)
+
+            val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
+            assertThat(actualDesktop?.tasksByTaskIdMap?.get(task.taskId)?.desktopTaskState)
+                .isEqualTo(DesktopTaskState.MINIMIZED)
+        }
+    }
+
+    @Test
     fun addOrUpdateTask_changeTaskStateToMinimize_taskStateIsMinimized() {
         runTest(StandardTestDispatcher()) {
             val task = createDesktopTask(1)
-            val DesktopPersistentRepositories = createRepositoryWithOneDesk(task)
-            testDatastore.updateData { DesktopPersistentRepositories }
+            val desktopPersistentRepositories = createRepositoryWithOneDesk(task)
+            testDatastore.updateData { desktopPersistentRepositories }
             // Create a new state to be initialized
             val visibleTasks = ArraySet(listOf(1))
             val minimizedTasks = ArraySet(listOf(1))
@@ -152,8 +187,8 @@
     fun removeTask_previouslyAddedTaskIsRemoved() {
         runTest(StandardTestDispatcher()) {
             val task = createDesktopTask(1)
-            val DesktopPersistentRepositories = createRepositoryWithOneDesk(task)
-            testDatastore.updateData { DesktopPersistentRepositories }
+            val desktopPersistentRepositories = createRepositoryWithOneDesk(task)
+            testDatastore.updateData { desktopPersistentRepositories }
             // Create a new state to be initialized
             val visibleTasks = ArraySet<Int>()
             val minimizedTasks = ArraySet<Int>()
@@ -176,17 +211,18 @@
     private companion object {
         const val DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE = "desktop_repo_test.pb"
         const val DEFAULT_USER_ID = 1000
+        const val USER_ID_2 = 2000
         const val DEFAULT_DESKTOP_ID = 0
 
         fun createRepositoryWithOneDesk(task: DesktopTask): DesktopPersistentRepositories {
             val desk = createDesktop(task)
             val repositoryState =
                 DesktopRepositoryState.newBuilder().putDesktop(DEFAULT_DESKTOP_ID, desk)
-            val DesktopPersistentRepositories =
+            val desktopPersistentRepositories =
                 DesktopPersistentRepositories.newBuilder()
                     .putDesktopRepoByUser(DEFAULT_USER_ID, repositoryState.build())
                     .build()
-            return DesktopPersistentRepositories
+            return desktopPersistentRepositories
         }
 
         fun createDesktop(task: DesktopTask): Desktop? =
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index b8629b2..fa5989a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -224,7 +224,7 @@
         task.displayId = INVALID_DISPLAY;
         mFreeformTaskListener.onTaskVanished(task);
 
-        verify(mDesktopUserRepositories.getCurrent(), never()).removeFreeformTask(task.displayId,
+        verify(mDesktopUserRepositories.getCurrent(), never()).removeTask(task.displayId,
                 task.taskId);
     }
 
@@ -248,7 +248,7 @@
                 .minimizeTask(task.displayId, task.taskId);
         verify(mDesktopUserRepositories.getCurrent()).removeClosingTask(task.taskId);
         verify(mDesktopUserRepositories.getCurrent())
-                .removeFreeformTask(task.displayId, task.taskId);
+                .removeTask(task.displayId, task.taskId);
     }
 
     @Test
@@ -265,7 +265,7 @@
         verify(mDesktopUserRepositories.getCurrent(), never())
                 .removeClosingTask(task.taskId);
         verify(mDesktopUserRepositories.getCurrent(), never())
-                .removeFreeformTask(task.displayId, task.taskId);
+                .removeTask(task.displayId, task.taskId);
     }
 
     @Test
diff --git a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
index b384a24..b0308b5 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
@@ -120,78 +120,90 @@
 
 key Q {
     label:                              'Q'
-    base, capslock+shift:               'q'
+    base:                               'q'
     shift, capslock:                    'Q'
+    shift+capslock:                     'q'
 }
 
 key W {
     label:                              'W'
-    base, capslock+shift:               'w'
+    base:                               'w'
     shift, capslock:                    'W'
+    shift+capslock:                     'w'
 }
 
 key E {
     label:                              'E'
-    base, capslock+shift:               'e'
+    base:                               'e'
     shift, capslock:                    'E'
+    shift+capslock:                     'e'
     ralt:                               '\u20ac'
 }
 
 key R {
     label:                              'R'
-    base, capslock+shift:               'r'
+    base:                               'r'
     shift, capslock:                    'R'
+    shift+capslock:                     'r'
 }
 
 key T {
     label:                              'T'
-    base, capslock+shift:               't'
+    base:                               't'
     shift, capslock:                    'T'
+    shift+capslock:                     't'
 }
 
 key Y {
     label:                              'Y'
-    base, capslock+shift:               'y'
+    base:                               'y'
     shift, capslock:                    'Y'
+    shift+capslock:                     'y'
 }
 
 key U {
     label:                              'U'
-    base, capslock+shift:               'u'
+    base:                               'u'
     shift, capslock:                    'U'
+    shift+capslock:                     'u'
 }
 
 key I {
     label:                              'I'
-    base, capslock+shift:               'i'
+    base:                               'i'
     shift, capslock:                    'I'
+    shift+capslock:                     'i'
 }
 
 key O {
     label:                              'O'
-    base, capslock+shift:               'o'
+    base:                               'o'
     shift, capslock:                    'O'
+    shift+capslock:                     'o'
 }
 
 key P {
     label:                              'P'
-    base, capslock+shift:               'p'
+    base:                               'p'
     shift, capslock:                    'P'
+    shift+capslock:                     'p'
     ralt:                               '\u00a7'
 }
 
 key LEFT_BRACKET {
     label:                              '\u0102'
-    base, capslock+shift:               '\u0103'
+    base:                               '\u0103'
     shift, capslock:                    '\u0102'
+    shift+capslock:                     '\u0103'
     ralt:                               '['
     ralt+shift:                         '{'
 }
 
 key RIGHT_BRACKET {
     label:                              '\u00ce'
-    base, capslock+shift:               '\u00ee'
+    base:                               '\u00ee'
     shift, capslock:                    '\u00ce'
+    shift+capslock:                     '\u00ee'
     ralt:                               ']'
     ralt+shift:                         '}'
 }
@@ -200,21 +212,24 @@
 
 key A {
     label:                              'A'
-    base, capslock+shift:               'a'
+    base:                               'a'
     shift, capslock:                    'A'
+    shift+capslock:                     'a'
 }
 
 key S {
     label:                              'S'
-    base, capslock+shift:               's'
+    base:                               's'
     shift, capslock:                    'S'
+    shift+capslock:                     's'
     ralt:                               '\u00df'
 }
 
 key D {
     label:                              'D'
-    base, capslock+shift:               'd'
+    base:                               'd'
     shift, capslock:                    'D'
+    shift+capslock:                     'd'
     ralt:                               '\u0111'
     ralt+shift, ralt+capslock:          '\u0110'
     ralt+shift+capslock:                '\u0111'
@@ -222,38 +237,44 @@
 
 key F {
     label:                              'F'
-    base, capslock+shift:               'f'
+    base:                               'f'
     shift, capslock:                    'F'
+    shift+capslock:                     'f'
 }
 
 key G {
     label:                              'G'
-    base, capslock+shift:               'g'
+    base:                               'g'
     shift, capslock:                    'G'
+    shift+capslock:                     'g'
 }
 
 key H {
     label:                              'H'
-    base, capslock+shift:               'h'
+    base:                               'h'
     shift, capslock:                    'H'
+    shift+capslock:                     'h'
 }
 
 key J {
     label:                              'J'
-    base, capslock+shift:               'j'
+    base:                               'j'
     shift, capslock:                    'J'
+    shift+capslock:                     'j'
 }
 
 key K {
     label:                              'K'
-    base, capslock+shift:               'k'
+    base:                               'k'
     shift, capslock:                    'K'
+    shift+capslock:                     'k'
 }
 
 key L {
     label:                              'L'
-    base, capslock+shift:               'l'
+    base:                               'l'
     shift, capslock:                    'L'
+    shift+capslock:                     'l'
     ralt:                               '\u0142'
     ralt+shift, ralt+capslock:          '\u0141'
     ralt+shift+capslock:                '\u0142'
@@ -261,24 +282,27 @@
 
 key SEMICOLON {
     label:                              '\u0218'
-    base, capslock+shift:               '\u0219'
+    base:                               '\u0219'
     shift, capslock:                    '\u0218'
+    shift+capslock:                     '\u0219'
     ralt:                               ';'
     ralt+shift:                         ':'
 }
 
 key APOSTROPHE {
     label:                              '\u021a'
-    base, capslock+shift:               '\u021b'
+    base:                               '\u021b'
     shift, capslock:                    '\u021a'
+    shift+capslock:                     '\u021b'
     ralt:                               '\''
     ralt+shift:                         '\u0022'
 }
 
 key BACKSLASH {
     label:                              '\u00c2'
-    base, capslock+shift:               '\u00e2'
+    base:                               '\u00e2'
     shift, capslock:                    '\u00c2'
+    shift+capslock:                     '\u00e2'
     ralt:                               '\\'
     ralt+shift:                         '|'
 }
@@ -293,45 +317,52 @@
 
 key Z {
     label:                              'Z'
-    base, capslock+shift:               'z'
+    base:                               'z'
     shift, capslock:                    'Z'
+    shift+capslock:                     'z'
 }
 
 key X {
     label:                              'X'
-    base, capslock+shift:               'x'
+    base:                               'x'
     shift, capslock:                    'X'
+    shift+capslock:                     'x'
 }
 
 key C {
     label:                              'C'
-    base, capslock+shift:               'c'
+    base:                               'c'
     shift, capslock:                    'C'
+    shift+capslock:                     'c'
     ralt:                               '\u00a9'
 }
 
 key V {
     label:                              'V'
-    base, capslock+shift:               'v'
+    base:                               'v'
     shift, capslock:                    'V'
+    shift+capslock:                     'v'
 }
 
 key B {
     label:                              'B'
-    base, capslock+shift:               'b'
+    base:                               'b'
     shift, capslock:                    'B'
+    shift+capslock:                     'b'
 }
 
 key N {
     label:                              'N'
-    base, capslock+shift:               'n'
+    base:                               'n'
     shift, capslock:                    'N'
+    shift+capslock:                     'n'
 }
 
 key M {
     label:                              'M'
-    base, capslock+shift:               'm'
+    base:                               'm'
     shift, capslock:                    'M'
+    shift+capslock:                     'm'
 }
 
 key COMMA {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm
index 6fa54f9..9df78c9 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm
@@ -104,149 +104,173 @@
 
 key Q {
     label:                              '\u0409'
-    base, capslock+shift:               '\u0459'
+    base:                               '\u0459'
     shift, capslock:                    '\u0409'
+    shift+capslock:                     '\u0459'
 }
 
 key W {
     label:                              '\u040a'
-    base, capslock+shift:               '\u045a'
+    base:                               '\u045a'
     shift, capslock:                    '\u040a'
+    shift+capslock:                     '\u045a'
 }
 
 key E {
     label:                              '\u0415'
-    base, capslock+shift:               '\u0435'
+    base:                               '\u0435'
     shift, capslock:                    '\u0415'
+    shift+capslock:                     '\u0435'
     ralt:                               '\u20ac'
 }
 
 key R {
     label:                              '\u0420'
-    base, capslock+shift:               '\u0440'
+    base:                               '\u0440'
     shift, capslock:                    '\u0420'
+    shift+capslock:                     '\u0440'
 }
 
 key T {
     label:                              '\u0422'
-    base, capslock+shift:               '\u0442'
+    base:                               '\u0442'
     shift, capslock:                    '\u0422'
+    shift+capslock:                     '\u0442'
 }
 
 key Y {
     label:                              '\u0417'
-    base, capslock+shift:               '\u0437'
+    base:                               '\u0437'
     shift, capslock:                    '\u0417'
+    shift+capslock:                     '\u0437'
 }
 
 key U {
     label:                              '\u0423'
-    base, capslock+shift:               '\u0443'
+    base:                               '\u0443'
     shift, capslock:                    '\u0423'
+    shift+capslock:                     '\u0443'
 }
 
 key I {
     label:                              '\u0418'
-    base, capslock+shift:               '\u0438'
+    base:                               '\u0438'
     shift, capslock:                    '\u0418'
+    shift+capslock:                     '\u0438'
 }
 
 key O {
     label:                              '\u041e'
-    base, capslock+shift:               '\u043e'
+    base:                               '\u043e'
     shift, capslock:                    '\u041e'
+    shift+capslock:                     '\u043e'
 }
 
 key P {
     label:                              '\u041f'
-    base, capslock+shift:               '\u043f'
+    base:                               '\u043f'
     shift, capslock:                    '\u041f'
+    shift+capslock:                     '\u043f'
 }
 
 key LEFT_BRACKET {
     label:                              '\u0428'
-    base, capslock+shift:               '\u0448'
+    base:                               '\u0448'
     shift, capslock:                    '\u0428'
+    shift+capslock:                     '\u0448'
 }
 
 key RIGHT_BRACKET {
     label:                              '\u0402'
-    base, capslock+shift:               '\u0452'
+    base:                               '\u0452'
     shift, capslock:                    '\u0402'
+    shift+capslock:                     '\u0452'
 }
 
 ### ROW 3
 
 key A {
     label:                              '\u0410'
-    base, capslock+shift:               '\u0430'
+    base:                               '\u0430'
     shift, capslock:                    '\u0410'
+    shift+capslock:                     '\u0430'
 }
 
 key S {
     label:                              '\u0421'
-    base, capslock+shift:               '\u0441'
+    base:                               '\u0441'
     shift, capslock:                    '\u0421'
+    shift+capslock:                     '\u0441'
 }
 
 key D {
     label:                              '\u0414'
-    base, capslock+shift:               '\u0434'
+    base:                               '\u0434'
     shift, capslock:                    '\u0414'
+    shift+capslock:                     '\u0434'
 }
 
 key F {
     label:                              '\u0424'
-    base, capslock+shift:               '\u0444'
+    base:                               '\u0444'
     shift, capslock:                    '\u0424'
+    shift+capslock:                     '\u0444'
 }
 
 key G {
     label:                              '\u0413'
-    base, capslock+shift:               '\u0433'
+    base:                               '\u0433'
     shift, capslock:                    '\u0413'
+    shift+capslock:                     '\u0433'
 }
 
 key H {
     label:                              '\u0425'
-    base, capslock+shift:               '\u0445'
+    base:                               '\u0445'
     shift, capslock:                    '\u0425'
+    shift+capslock:                     '\u0445'
 }
 
 key J {
     label:                              '\u0408'
-    base, capslock+shift:               '\u0458'
+    base:                               '\u0458'
     shift, capslock:                    '\u0408'
+    shift+capslock:                     '\u0458'
 }
 
 key K {
     label:                              '\u041a'
-    base, capslock+shift:               '\u043a'
+    base:                               '\u043a'
     shift, capslock:                    '\u041a'
+    shift+capslock:                     '\u043a'
 }
 
 key L {
     label:                              '\u041b'
-    base, capslock+shift:               '\u043b'
+    base:                               '\u043b'
     shift, capslock:                    '\u041b'
+    shift+capslock:                     '\u043b'
 }
 
 key SEMICOLON {
     label:                              '\u0427'
-    base, capslock+shift:               '\u0447'
+    base:                               '\u0447'
     shift, capslock:                    '\u0427'
+    shift+capslock:                     '\u0447'
 }
 
 key APOSTROPHE {
     label:                              '\u040b'
-    base, capslock+shift:               '\u045b'
+    base:                               '\u045b'
     shift, capslock:                    '\u040b'
+    shift+capslock:                     '\u045b'
 }
 
 key BACKSLASH {
     label:                              '\u0416'
-    base, capslock+shift:               '\u0436'
+    base:                               '\u0436'
     shift, capslock:                    '\u0416'
+    shift+capslock:                     '\u0436'
 }
 
 ### ROW 4
@@ -259,44 +283,51 @@
 
 key Z {
     label:                              '\u0405'
-    base, capslock+shift:               '\u0455'
+    base:                               '\u0455'
     shift, capslock:                    '\u0405'
+    shift+capslock:                     '\u0455'
 }
 
 key X {
     label:                              '\u040f'
-    base, capslock+shift:               '\u045f'
+    base:                               '\u045f'
     shift, capslock:                    '\u040f'
+    shift+capslock:                     '\u045f'
 }
 
 key C {
     label:                              '\u0426'
-    base, capslock+shift:               '\u0446'
+    base:                               '\u0446'
     shift, capslock:                    '\u0426'
+    shift+capslock:                     '\u0446'
 }
 
 key V {
     label:                              '\u0412'
-    base, capslock+shift:               '\u0432'
+    base:                               '\u0432'
     shift, capslock:                    '\u0412'
+    shift+capslock:                     '\u0432'
 }
 
 key B {
     label:                              '\u0411'
-    base, capslock+shift:               '\u0431'
+    base:                               '\u0431'
     shift, capslock:                    '\u0411'
+    shift+capslock:                     '\u0431'
 }
 
 key N {
     label:                              '\u041d'
-    base, capslock+shift:               '\u043d'
+    base:                               '\u043d'
     shift, capslock:                    '\u041d'
+    shift+capslock:                     '\u043d'
 }
 
 key M {
     label:                              '\u041c'
-    base, capslock+shift:               '\u043c'
+    base:                               '\u043c'
     shift, capslock:                    '\u041c'
+    shift+capslock:                     '\u043c'
 }
 
 key COMMA {
@@ -317,4 +348,4 @@
     label:                              '-'
     base:                               '-'
     shift:                              '_'
-}
\ No newline at end of file
+}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm
index 8e4d7b1..4c8997b 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm
@@ -120,78 +120,90 @@
 
 key Q {
     label:                              'Q'
-    base, capslock+shift:               'q'
+    base:                               'q'
     shift, capslock:                    'Q'
+    shift+capslock:                     'q'
     ralt:                               '\\'
 }
 
 key W {
     label:                              'W'
-    base, capslock+shift:               'w'
+    base:                               'w'
     shift, capslock:                    'W'
+    shift+capslock:                     'w'
     ralt:                               '|'
 }
 
 key E {
     label:                              'E'
-    base, capslock+shift:               'e'
+    base:                               'e'
     shift, capslock:                    'E'
+    shift+capslock:                     'e'
     ralt:                               '\u20ac'
 }
 
 key R {
     label:                              'R'
-    base, capslock+shift:               'r'
+    base:                               'r'
     shift, capslock:                    'R'
+    shift+capslock:                     'r'
 }
 
 key T {
     label:                              'T'
-    base, capslock+shift:               't'
+    base:                               't'
     shift, capslock:                    'T'
+    shift+capslock:                     't'
 }
 
 key Z {
     label:                              'Z'
-    base, capslock+shift:               'z'
+    base:                               'z'
     shift, capslock:                    'Z'
+    shift+capslock:                     'z'
 }
 
 key U {
     label:                              'U'
-    base, capslock+shift:               'u'
+    base:                               'u'
     shift, capslock:                    'U'
+    shift+capslock:                     'u'
 }
 
 key I {
     label:                              'I'
-    base, capslock+shift:               'i'
+    base:                               'i'
     shift, capslock:                    'I'
+    shift+capslock:                     'i'
 }
 
 key O {
     label:                              'O'
-    base, capslock+shift:               'o'
+    base:                               'o'
     shift, capslock:                    'O'
+    shift+capslock:                     'o'
 }
 
 key P {
     label:                              'P'
-    base, capslock+shift:               'p'
+    base:                               'p'
     shift, capslock:                    'P'
+    shift+capslock:                     'p'
 }
 
 key LEFT_BRACKET {
     label:                              '\u0160'
-    base, capslock+shift:               '\u0161'
+    base:                               '\u0161'
     shift, capslock:                    '\u0160'
+    shift+capslock:                     '\u0161'
     ralt:                               '\u00f7'
 }
 
 key RIGHT_BRACKET {
     label:                              '\u0110'
-    base, capslock+shift:               '\u0111'
+    base:                               '\u0111'
     shift, capslock:                    '\u0110'
+    shift+capslock:                     '\u0111'
     ralt:                               '\u00d7'
 }
 
@@ -199,79 +211,91 @@
 
 key A {
     label:                              'A'
-    base, capslock+shift:               'a'
+    base:                               'a'
     shift, capslock:                    'A'
+    shift+capslock:                     'a'
 }
 
 key S {
     label:                              'S'
-    base, capslock+shift:               's'
+    base:                               's'
     shift, capslock:                    'S'
+    shift+capslock:                     's'
 }
 
 key D {
     label:                              'D'
-    base, capslock+shift:               'd'
+    base:                               'd'
     shift, capslock:                    'D'
+    shift+capslock:                     'd'
 }
 
 key F {
     label:                              'F'
-    base, capslock+shift:               'f'
+    base:                               'f'
     shift, capslock:                    'F'
+    shift+capslock:                     'f'
     ralt:                               '['
 }
 
 key G {
     label:                              'G'
-    base, capslock+shift:               'g'
+    base:                               'g'
     shift, capslock:                    'G'
+    shift+capslock:                     'g'
     ralt:                               ']'
 }
 
 key H {
     label:                              'H'
-    base, capslock+shift:               'h'
+    base:                               'h'
     shift, capslock:                    'H'
+    shift+capslock:                     'h'
 }
 
 key J {
     label:                              'J'
-    base, capslock+shift:               'j'
+    base:                               'j'
     shift, capslock:                    'J'
+    shift+capslock:                     'j'
 }
 
 key K {
     label:                              'K'
-    base, capslock+shift:               'k'
+    base:                               'k'
     shift, capslock:                    'K'
+    shift+capslock:                     'k'
     ralt:                               '\u0142'
 }
 
 key L {
     label:                              'L'
-    base, capslock+shift:               'l'
+    base:                               'l'
     shift, capslock:                    'L'
+    shift+capslock:                     'l'
     ralt:                               '\u0141'
 }
 
 key SEMICOLON {
     label:                              '\u010c'
-    base, capslock+shift:               '\u010d'
+    base:                               '\u010d'
     shift, capslock:                    '\u010c'
+    shift+capslock:                     '\u010d'
 }
 
 key APOSTROPHE {
     label:                              '\u0106'
-    base, capslock+shift:               '\u0107'
+    base:                               '\u0107'
     shift, capslock:                    '\u0106'
+    shift+capslock:                     '\u0107'
     ralt:                               '\u00df'
 }
 
 key BACKSLASH {
     label:                              '\u017d'
-    base, capslock+shift:               '\u017e'
+    base:                               '\u017e'
     shift, capslock:                    '\u017d'
+    shift+capslock:                     '\u017e'
     ralt:                               '\u00a4'
 }
 
@@ -285,47 +309,54 @@
 
 key Y {
     label:                              'Y'
-    base, capslock+shift:               'y'
+    base:                               'y'
     shift, capslock:                    'Y'
+    shift+capslock:                     'y'
 }
 
 key X {
     label:                              'X'
-    base, capslock+shift:               'x'
+    base:                               'x'
     shift, capslock:                    'X'
+    shift+capslock:                     'x'
 }
 
 key C {
     label:                              'C'
-    base, capslock+shift:               'c'
+    base:                               'c'
     shift, capslock:                    'C'
+    shift+capslock:                     'c'
 }
 
 key V {
     label:                              'V'
-    base, capslock+shift:               'v'
+    base:                               'v'
     shift, capslock:                    'V'
+    shift+capslock:                     'v'
     ralt:                               '@'
 }
 
 key B {
     label:                              'B'
-    base, capslock+shift:               'b'
+    base:                               'b'
     shift, capslock:                    'B'
+    shift+capslock:                     'b'
     ralt:                               '{'
 }
 
 key N {
     label:                              'N'
-    base, capslock+shift:               'n'
+    base:                               'n'
     shift, capslock:                    'N'
+    shift+capslock:                     'n'
     ralt:                               '}'
 }
 
 key M {
     label:                              'M'
-    base, capslock+shift:               'm'
+    base:                               'm'
     shift, capslock:                    'M'
+    shift+capslock:                     'm'
     ralt:                               '\u00a7'
 }
 
@@ -347,4 +378,4 @@
     label:                              '-'
     base:                               '-'
     shift:                              '_'
-}
\ No newline at end of file
+}
diff --git a/packages/PrintSpooler/Android.bp b/packages/PrintSpooler/Android.bp
index 000e20f..5c5ec69 100644
--- a/packages/PrintSpooler/Android.bp
+++ b/packages/PrintSpooler/Android.bp
@@ -47,20 +47,23 @@
     resource_dirs: ["res"],
     srcs: [
         "src/**/*.java",
-        "src/com/android/printspooler/renderer/IPdfRenderer.aidl",
         "src/com/android/printspooler/renderer/IPdfEditor.aidl",
+        "src/com/android/printspooler/renderer/IPdfRenderer.aidl",
     ],
     platform_apis: true,
     static_libs: [
-        "android-support-v7-recyclerview",
-        "android-support-compat",
-        "android-support-media-compat",
-        "android-support-core-utils",
-        "android-support-core-ui",
-        "android-support-fragment",
         "android-support-annotations",
+        "android-support-compat",
+        "android-support-core-ui",
+        "android-support-core-utils",
+        "android-support-fragment",
+        "android-support-media-compat",
+        "android-support-v7-recyclerview",
         "printspooler_aconfig_flags_java_lib",
     ],
+    flags_packages: [
+        "printspooler_aconfig_declarations",
+    ],
     manifest: "AndroidManifest.xml",
 }
 
diff --git a/packages/PrintSpooler/flags/flags.aconfig b/packages/PrintSpooler/flags/flags.aconfig
index 4a76dff..5d45b3f 100644
--- a/packages/PrintSpooler/flags/flags.aconfig
+++ b/packages/PrintSpooler/flags/flags.aconfig
@@ -7,3 +7,14 @@
   description: "Log print job creation and state transitions."
   bug: "385340868"
 }
+
+flag {
+    name: "print_edge2edge"
+    namespace: "printing"
+    description: "Enable edge to edge in print spooler"
+    bug: "378652618"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
\ No newline at end of file
diff --git a/packages/PrintSpooler/res/layout/select_printer_activity.xml b/packages/PrintSpooler/res/layout/select_printer_activity.xml
index 681924b..c5f19b1 100644
--- a/packages/PrintSpooler/res/layout/select_printer_activity.xml
+++ b/packages/PrintSpooler/res/layout/select_printer_activity.xml
@@ -15,6 +15,7 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/select_printer"
     android:orientation="vertical"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent">
diff --git a/packages/PrintSpooler/res/values-night/themes.xml b/packages/PrintSpooler/res/values-night/themes.xml
index 76fa7b9..495bbcac 100644
--- a/packages/PrintSpooler/res/values-night/themes.xml
+++ b/packages/PrintSpooler/res/values-night/themes.xml
@@ -15,7 +15,7 @@
   limitations under the License.
   -->
 
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <style name="Theme.AddPrinterActivity" parent="@android:style/Theme.DeviceDefault.Dialog">
         <item name="android:listSeparatorTextViewStyle">@style/ListSeparator</item>
         <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item>
@@ -24,14 +24,14 @@
     <style name="Theme.SelectPrinterActivity"
            parent="android:style/Theme.DeviceDefault">
         <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item>
-        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
+        <item name="android:windowOptOutEdgeToEdgeEnforcement" android:featureFlag="!com.android.printspooler.flags.print_edge2edge">true</item>
     </style>
 
     <style name="Theme.PrintActivity" parent="@android:style/Theme.DeviceDefault">
         <item name="android:windowIsTranslucent">true</item>
         <item name="android:windowActionBar">false</item>
         <item name="android:windowNoTitle">true</item>
-        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
+        <item name="android:windowOptOutEdgeToEdgeEnforcement" android:featureFlag="!com.android.printspooler.flags.print_edge2edge">true</item>
     </style>
 
 </resources>
diff --git a/packages/PrintSpooler/res/values/themes.xml b/packages/PrintSpooler/res/values/themes.xml
index 22842f7..5fcbbf5 100644
--- a/packages/PrintSpooler/res/values/themes.xml
+++ b/packages/PrintSpooler/res/values/themes.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 
-<resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
     <style name="Theme.AddPrinterActivity" parent="@android:style/Theme.DeviceDefault.Light.Dialog">
         <item name="android:listSeparatorTextViewStyle">@style/ListSeparator</item>
         <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item>
@@ -24,7 +24,7 @@
            parent="android:style/Theme.DeviceDefault.Light">
         <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item>
         <item name="android:windowLightStatusBar">true</item>
-        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
+        <item name="android:windowOptOutEdgeToEdgeEnforcement" android:featureFlag="!com.android.printspooler.flags.print_edge2edge">true</item>
     </style>
 
     <style name="Theme.PrintActivity" parent="@android:style/Theme.DeviceDefault.Light">
@@ -32,7 +32,7 @@
         <item name="android:windowActionBar">false</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowLightStatusBar">true</item>
-        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
+        <item name="android:windowOptOutEdgeToEdgeEnforcement" android:featureFlag="!com.android.printspooler.flags.print_edge2edge">true</item>
     </style>
 
 </resources>
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
index 74acf67..559d3ea 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
@@ -66,10 +66,10 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.printspooler.R;
+import com.android.printspooler.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.List;
-
 /**
  * This is an activity for selecting a printer.
  */
@@ -134,6 +134,8 @@
         mPrinterRegistry = new PrinterRegistry(this, null, LOADER_ID_PRINT_REGISTRY,
                 LOADER_ID_PRINT_REGISTRY_INT);
 
+        findViewById(R.id.select_printer).setFitsSystemWindows(Flags.printEdge2edge());
+
         // Hook up the list view.
         mListView = findViewById(android.R.id.list);
         final DestinationAdapter adapter = new DestinationAdapter();
diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java
index 6ecffa4..720d5b1 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java
@@ -25,6 +25,7 @@
 import android.view.inputmethod.InputMethodManager;
 
 import com.android.printspooler.R;
+import com.android.printspooler.flags.Flags;
 
 /**
  * This class is a layout manager for the print screen. It has a sliding
@@ -93,6 +94,7 @@
         // The options view is sliding under the static header but appears
         // after it in the layout, so we will draw in opposite order.
         setChildrenDrawingOrderEnabled(true);
+        setFitsSystemWindows(Flags.printEdge2edge());
     }
 
     public void setOptionsStateChangeListener(OptionsStateChangeListener listener) {
@@ -148,6 +150,7 @@
         mExpandCollapseHandle = findViewById(R.id.expand_collapse_handle);
         mExpandCollapseIcon = findViewById(R.id.expand_collapse_icon);
 
+        mOptionsContainer.setFitsSystemWindows(Flags.printEdge2edge());
         mExpandCollapseHandle.setOnClickListener(this);
         mSummaryContent.setOnClickListener(this);
 
@@ -262,7 +265,7 @@
         }
 
         // The content host can grow vertically as much as needed - we will be covering it.
-        final int hostHeightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0);
+        final int hostHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
         measureChild(mEmbeddedContentContainer, widthMeasureSpec, hostHeightMeasureSpec);
 
         setMeasuredDimension(resolveSize(MeasureSpec.getSize(widthMeasureSpec), widthMeasureSpec),
@@ -271,25 +274,43 @@
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        mStaticContent.layout(left, top, right, mStaticContent.getMeasuredHeight());
+        final int childLeft;
+        final int childRight;
+        final int childTop;
+        if (Flags.printEdge2edge()) {
+            childLeft = left + mPaddingLeft;
+            childRight = right - mPaddingRight;
+            childTop = top + mPaddingTop;
+        } else {
+            childLeft = left;
+            childRight = right;
+            childTop = top;
+        }
+        mStaticContent.layout(childLeft, childTop, childRight,
+                mStaticContent.getMeasuredHeight() + (Flags.printEdge2edge() ? mPaddingTop : 0));
 
         if (mSummaryContent.getVisibility() != View.GONE) {
-            mSummaryContent.layout(left, mStaticContent.getMeasuredHeight(), right,
-                    mStaticContent.getMeasuredHeight() + mSummaryContent.getMeasuredHeight());
+            mSummaryContent.layout(childLeft,
+                    (Flags.printEdge2edge() ? mStaticContent.getBottom()
+                            : mStaticContent.getMeasuredHeight()), childRight,
+                    (Flags.printEdge2edge() ? mStaticContent.getBottom()
+                            : mStaticContent.getMeasuredHeight())
+                                + mSummaryContent.getMeasuredHeight());
         }
 
-        final int dynContentTop = mStaticContent.getMeasuredHeight() + mCurrentOptionsOffsetY;
+        final int dynContentTop = mStaticContent.getBottom() + mCurrentOptionsOffsetY;
         final int dynContentBottom = dynContentTop + mDynamicContent.getMeasuredHeight();
 
-        mDynamicContent.layout(left, dynContentTop, right, dynContentBottom);
+        mDynamicContent.layout(childLeft, dynContentTop, childRight, dynContentBottom);
 
         MarginLayoutParams params = (MarginLayoutParams) mPrintButton.getLayoutParams();
 
         final int printButtonLeft;
         if (getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
-            printButtonLeft = right - mPrintButton.getMeasuredWidth() - params.getMarginStart();
+            printButtonLeft = childRight - mPrintButton.getMeasuredWidth()
+                    - params.getMarginStart();
         } else {
-            printButtonLeft = left + params.getMarginStart();
+            printButtonLeft = childLeft + params.getMarginStart();
         }
         final int printButtonTop = dynContentBottom - mPrintButton.getMeasuredHeight() / 2;
         final int printButtonRight = printButtonLeft + mPrintButton.getMeasuredWidth();
@@ -297,11 +318,13 @@
 
         mPrintButton.layout(printButtonLeft, printButtonTop, printButtonRight, printButtonBottom);
 
-        final int embContentTop = mStaticContent.getMeasuredHeight() + mClosedOptionsOffsetY
-                + mDynamicContent.getMeasuredHeight();
-        final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight();
+        final int embContentTop = (Flags.printEdge2edge() ? mPaddingTop : 0)
+                + mStaticContent.getMeasuredHeight()
+                + mClosedOptionsOffsetY + mDynamicContent.getMeasuredHeight();
+        final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight()
+                -  (Flags.printEdge2edge() ? mPaddingBottom : 0);
 
-        mEmbeddedContentContainer.layout(left, embContentTop, right, embContentBottom);
+        mEmbeddedContentContainer.layout(childLeft, embContentTop, childRight, embContentBottom);
     }
 
     @Override
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 46bd88f..4448000 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -169,6 +169,7 @@
     <!-- Internal permissions granted to the shell. -->
     <uses-permission android:name="android.permission.FORCE_BACK" />
     <uses-permission android:name="android.permission.BATTERY_STATS" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_POWER_MONITORS" />
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
     <uses-permission android:name="android.permission.REPORT_USAGE_STATS" />
     <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 70d4cc2..a0d7fd2 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -84,6 +84,16 @@
 }
 
 flag {
+   name: "notification_ambient_suppression_after_inflation"
+   namespace: "systemui"
+   description: "Move the DND visual effects filter to the finalize stage of the pipeline when it is doze-dependent, but keep it in the pre-group stage when it is doze-independent."
+   bug: "373411431"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
    name: "notification_over_expansion_clipping_fix"
    namespace: "systemui"
    description: "Fix NSSL clipping when over-expanding; fixes split shade bug."
@@ -382,14 +392,6 @@
 }
 
 flag {
-    name: "status_bar_screen_sharing_chips"
-    namespace: "systemui"
-    description: "Show chips on the left side of the status bar when a user is screen sharing, "
-        "recording, or casting"
-    bug: "332662551"
-}
-
-flag {
     name: "status_bar_show_audio_only_projection_chip"
     namespace: "systemui"
     description: "Show chip on the left side of the status bar when a user is only sharing *audio* "
@@ -1795,16 +1797,6 @@
 }
 
 flag {
-  name: "disable_shade_expands_on_trackpad_two_finger_swipe"
-  namespace: "systemui"
-  description: "Disables expansion of the shade via two finger swipe on a trackpad"
-  bug: "356804470"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
     name: "keyboard_shortcut_helper_shortcut_customizer"
     namespace: "systemui"
     description: "An implementation of shortcut customizations through shortcut helper."
@@ -1905,6 +1897,16 @@
 }
 
 flag {
+  name: "disable_shade_trackpad_two_finger_swipe"
+  namespace: "systemui"
+  description: "Disables expansion of the shade via two finger swipe on a trackpad"
+  bug: "356804470"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
    name: "notification_magic_actions_treatment"
    namespace: "systemui"
    description: "Special UI treatment for magic actions"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index b0c7ac0..c8d3430 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -232,19 +232,21 @@
     private val lifecycleListener =
         object : Listener {
             override fun onTransitionAnimationStart() {
-                listeners.forEach { it.onTransitionAnimationStart() }
+                LinkedHashSet(listeners).forEach { it.onTransitionAnimationStart() }
             }
 
             override fun onTransitionAnimationEnd() {
-                listeners.forEach { it.onTransitionAnimationEnd() }
+                LinkedHashSet(listeners).forEach { it.onTransitionAnimationEnd() }
             }
 
             override fun onTransitionAnimationProgress(linearProgress: Float) {
-                listeners.forEach { it.onTransitionAnimationProgress(linearProgress) }
+                LinkedHashSet(listeners).forEach {
+                     it.onTransitionAnimationProgress(linearProgress)
+                }
             }
 
             override fun onTransitionAnimationCancelled() {
-                listeners.forEach { it.onTransitionAnimationCancelled() }
+                LinkedHashSet(listeners).forEach { it.onTransitionAnimationCancelled() }
             }
         }
 
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
index 96401ce..a27bf8a 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
@@ -444,7 +444,7 @@
         val left = available - consumed
         val postConsumed =
             nestedScrollDispatcher.dispatchPostScroll(
-                consumed = preConsumed + consumed,
+                consumed = consumed,
                 available = left,
                 source = NestedScrollSource.UserInput,
             )
@@ -482,10 +482,9 @@
         val available = velocity - preConsumed
         val consumed = performFling(available)
         val left = available - consumed
-        return nestedScrollDispatcher.dispatchPostFling(
-            consumed = consumed + preConsumed,
-            available = left,
-        )
+        val postConsumed =
+            nestedScrollDispatcher.dispatchPostFling(consumed = consumed, available = left)
+        return preConsumed + consumed + postConsumed
     }
 
     /*
@@ -549,9 +548,10 @@
             nestedScrollController == null &&
                 // TODO(b/388231324): Remove this.
                 !lastEventWasScrollWheel &&
-                draggable.shouldConsumeNestedScroll(sign)
+                draggable.shouldConsumeNestedScroll(sign) &&
+                lastFirstDown != null
         ) {
-            val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" }
+            val startedPosition = checkNotNull(lastFirstDown)
 
             // TODO(b/382665591): Ensure that there is at least one pointer down.
             val pointersDownCount = pointersDown.size.coerceAtLeast(1)
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
index 5de0f12..19d28cc 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
@@ -36,6 +36,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.PointerType
@@ -52,6 +53,7 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeWithVelocity
 import androidx.compose.ui.unit.Velocity
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.ceil
@@ -773,6 +775,181 @@
         rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3")
     }
 
+    @Test
+    fun nestedDragNotStartedWhenEnabledAfterDragStarted() {
+        val draggable = TestDraggable()
+        var enabled by mutableStateOf(false)
+        val touchSlop =
+            rule.setContentWithTouchSlop {
+                Box(
+                    Modifier.fillMaxSize()
+                        .nestedDraggable(draggable, orientation, enabled = enabled)
+                        .scrollable(rememberScrollableState { 0f }, orientation)
+                )
+            }
+
+        rule.onRoot().performTouchInput { down(center) }
+
+        enabled = true
+        rule.waitForIdle()
+
+        rule.onRoot().performTouchInput { moveBy((touchSlop + 1f).toOffset()) }
+
+        assertThat(draggable.onDragStartedCalled).isFalse()
+    }
+
+    @Test
+    fun availableAndConsumedScrollDeltas() {
+        val totalScroll = 200f
+        val consumedByEffectPreScroll = 10f // 200f => 190f
+        val consumedByConnectionPreScroll = 20f // 190f => 170f
+        val consumedByScroll = 30f // 170f => 140f
+        val consumedByConnectionPostScroll = 40f // 140f => 100f
+
+        // Available scroll values that we will check later.
+        var availableToEffectPreScroll = 0f
+        var availableToConnectionPreScroll = 0f
+        var availableToScroll = 0f
+        var availableToConnectionPostScroll = 0f
+        var availableToEffectPostScroll = 0f
+
+        val effect =
+            TestOverscrollEffect(
+                orientation,
+                onPreScroll = {
+                    availableToEffectPreScroll = it
+                    consumedByEffectPreScroll
+                },
+                onPostScroll = {
+                    availableToEffectPostScroll = it
+                    it
+                },
+            )
+
+        val connection =
+            object : NestedScrollConnection {
+                override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                    availableToConnectionPreScroll = available.toFloat()
+                    return consumedByConnectionPreScroll.toOffset()
+                }
+
+                override fun onPostScroll(
+                    consumed: Offset,
+                    available: Offset,
+                    source: NestedScrollSource,
+                ): Offset {
+                    assertThat(consumed.toFloat()).isEqualTo(consumedByScroll)
+                    availableToConnectionPostScroll = available.toFloat()
+                    return consumedByConnectionPostScroll.toOffset()
+                }
+            }
+
+        val draggable =
+            TestDraggable(
+                onDrag = {
+                    availableToScroll = it
+                    consumedByScroll
+                }
+            )
+
+        val touchSlop =
+            rule.setContentWithTouchSlop {
+                Box(
+                    Modifier.fillMaxSize()
+                        .nestedScroll(connection)
+                        .nestedDraggable(draggable, orientation, effect)
+                )
+            }
+
+        rule.onRoot().performTouchInput {
+            down(center)
+            moveBy((touchSlop + totalScroll).toOffset())
+        }
+
+        assertThat(availableToEffectPreScroll).isEqualTo(200f)
+        assertThat(availableToConnectionPreScroll).isEqualTo(190f)
+        assertThat(availableToScroll).isEqualTo(170f)
+        assertThat(availableToConnectionPostScroll).isEqualTo(140f)
+        assertThat(availableToEffectPostScroll).isEqualTo(100f)
+    }
+
+    @Test
+    fun availableAndConsumedVelocities() {
+        val totalVelocity = 200f
+        val consumedByEffectPreFling = 10f // 200f => 190f
+        val consumedByConnectionPreFling = 20f // 190f => 170f
+        val consumedByFling = 30f // 170f => 140f
+        val consumedByConnectionPostFling = 40f // 140f => 100f
+
+        // Available velocities that we will check later.
+        var availableToEffectPreFling = 0f
+        var availableToConnectionPreFling = 0f
+        var availableToFling = 0f
+        var availableToConnectionPostFling = 0f
+        var availableToEffectPostFling = 0f
+
+        val effect =
+            TestOverscrollEffect(
+                orientation,
+                onPreFling = {
+                    availableToEffectPreFling = it
+                    consumedByEffectPreFling
+                },
+                onPostFling = {
+                    availableToEffectPostFling = it
+                    it
+                },
+                onPostScroll = { 0f },
+            )
+
+        val connection =
+            object : NestedScrollConnection {
+                override suspend fun onPreFling(available: Velocity): Velocity {
+                    availableToConnectionPreFling = available.toFloat()
+                    return consumedByConnectionPreFling.toVelocity()
+                }
+
+                override suspend fun onPostFling(
+                    consumed: Velocity,
+                    available: Velocity,
+                ): Velocity {
+                    assertThat(consumed.toFloat()).isEqualTo(consumedByFling)
+                    availableToConnectionPostFling = available.toFloat()
+                    return consumedByConnectionPostFling.toVelocity()
+                }
+            }
+
+        val draggable =
+            TestDraggable(
+                onDragStopped = { velocity, _ ->
+                    availableToFling = velocity
+                    consumedByFling
+                },
+                onDrag = { 0f },
+            )
+
+        rule.setContent {
+            Box(
+                Modifier.fillMaxSize()
+                    .nestedScroll(connection)
+                    .nestedDraggable(draggable, orientation, effect)
+            )
+        }
+
+        rule.onRoot().performTouchInput {
+            when (orientation) {
+                Orientation.Horizontal -> swipeWithVelocity(topLeft, topRight, totalVelocity)
+                Orientation.Vertical -> swipeWithVelocity(topLeft, bottomLeft, totalVelocity)
+            }
+        }
+
+        assertThat(availableToEffectPreFling).isWithin(1f).of(200f)
+        assertThat(availableToConnectionPreFling).isWithin(1f).of(190f)
+        assertThat(availableToFling).isWithin(1f).of(170f)
+        assertThat(availableToConnectionPostFling).isWithin(1f).of(140f)
+        assertThat(availableToEffectPostFling).isWithin(1f).of(100f)
+    }
+
     private fun ComposeContentTestRule.setContentWithTouchSlop(
         content: @Composable () -> Unit
     ): Float {
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
index 8bf9c21..0659f919 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
@@ -24,6 +24,8 @@
 
 class TestOverscrollEffect(
     override val orientation: Orientation,
+    private val onPreScroll: (Float) -> Float = { 0f },
+    private val onPreFling: suspend (Float) -> Float = { 0f },
     private val onPostFling: suspend (Float) -> Float = { it },
     private val onPostScroll: (Float) -> Float,
 ) : OverscrollEffect, OrientationAware {
@@ -36,19 +38,23 @@
         source: NestedScrollSource,
         performScroll: (Offset) -> Offset,
     ): Offset {
-        val consumedByScroll = performScroll(delta)
-        val available = delta - consumedByScroll
-        val consumedByEffect = onPostScroll(available.toFloat()).toOffset()
-        return consumedByScroll + consumedByEffect
+        val consumedByPreScroll = onPreScroll(delta.toFloat()).toOffset()
+        val availableToScroll = delta - consumedByPreScroll
+        val consumedByScroll = performScroll(availableToScroll)
+        val availableToPostScroll = availableToScroll - consumedByScroll
+        val consumedByPostScroll = onPostScroll(availableToPostScroll.toFloat()).toOffset()
+        return consumedByPreScroll + consumedByScroll + consumedByPostScroll
     }
 
     override suspend fun applyToFling(
         velocity: Velocity,
         performFling: suspend (Velocity) -> Velocity,
     ) {
-        val consumedByFling = performFling(velocity)
-        val available = velocity - consumedByFling
-        onPostFling(available.toFloat())
+        val consumedByPreFling = onPreFling(velocity.toFloat()).toVelocity()
+        val availableToFling = velocity - consumedByPreFling
+        val consumedByFling = performFling(availableToFling)
+        val availableToPostFling = availableToFling - consumedByFling
+        onPostFling(availableToPostFling.toFloat())
         applyToFlingDone = true
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 6d24fc16..aa8b4ae 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -48,8 +48,8 @@
 val SceneContainerTransitions = transitions {
     interruptionHandler = SceneContainerInterruptionHandler
 
-    // Overscroll progress starts linearly with some resistance (3f) and slowly approaches 0.2f
-    defaultSwipeSpec = spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f)
+    defaultMotionSpatialSpec =
+        spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f)
 
     // Scene transitions
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
index ce7a85b1..e30e7d3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
@@ -30,7 +30,7 @@
 
 fun TransitionBuilder.goneToSplitShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
-    swipeSpec =
+    motionSpatialSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
index 1f7a738..1a243ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt
@@ -29,7 +29,7 @@
 
 fun TransitionBuilder.lockscreenToSplitShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
-    swipeSpec =
+    motionSpatialSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt
index 24f285e..a9af95b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt
@@ -27,7 +27,7 @@
     durationScale: Double = 1.0
 ) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
-    swipeSpec =
+    motionSpatialSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
index 3d62151..ddea585 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
@@ -31,7 +31,7 @@
 
 fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
-    swipeSpec =
+    motionSpatialSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
index e78bc6a..e477a41 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
@@ -28,7 +28,7 @@
 
 fun TransitionBuilder.toQuickSettingsShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
-    swipeSpec =
+    motionSpatialSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
index bfae489..4db4934 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
@@ -32,7 +32,7 @@
 
 fun TransitionBuilder.toShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
-    swipeSpec =
+    motionSpatialSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 2ca8464..633328a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -16,62 +16,29 @@
 
 package com.android.compose.animation.scene
 
+import androidx.compose.foundation.OverscrollEffect
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
 import androidx.compose.ui.util.fastCoerceIn
 import com.android.compose.animation.scene.content.Content
 import com.android.compose.animation.scene.content.state.TransitionState.Companion.DistanceUnspecified
-import com.android.compose.nestedscroll.OnStopScope
-import com.android.compose.nestedscroll.PriorityNestedScrollConnection
-import com.android.compose.nestedscroll.ScrollController
+import com.android.compose.animation.scene.effect.GestureEffect
+import com.android.compose.gesture.NestedDraggable
 import com.android.compose.ui.util.SpaceVectorConverter
 import kotlin.math.absoluteValue
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.NonCancellable
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 
-internal interface DraggableHandler {
-    /**
-     * Start a drag with the given [pointersDown] and [overSlop].
-     *
-     * The returned [DragController] should be used to continue or stop the drag.
-     */
-    fun onDragStarted(pointersDown: PointersInfo.PointersDown?, overSlop: Float): DragController
-}
-
-/**
- * The [DragController] provides control over the transition between two scenes through the [onDrag]
- * and [onStop] methods.
- */
-internal interface DragController {
-    /**
-     * Drag the current scene by [delta] pixels.
-     *
-     * @param delta The distance to drag the scene in pixels.
-     * @return the consumed [delta]
-     */
-    fun onDrag(delta: Float): Float
-
-    /**
-     * Stop the current drag with the given [velocity].
-     *
-     * @param velocity The velocity of the drag when it stopped.
-     * @return the consumed [velocity] when the animation complete
-     */
-    suspend fun onStop(velocity: Float): Float
-
-    /** Cancels the current drag. */
-    fun onCancel()
-}
-
-internal class DraggableHandlerImpl(
+internal class DraggableHandler(
     internal val layoutImpl: SceneTransitionLayoutImpl,
     internal val orientation: Orientation,
-) : DraggableHandler {
+    private val gestureEffectProvider: (ContentKey) -> GestureEffect,
+) : NestedDraggable {
     /** The [DraggableHandler] can only have one active [DragController] at a time. */
     private var dragController: DragControllerImpl? = null
 
@@ -92,20 +59,36 @@
     internal val positionalThreshold
         get() = with(layoutImpl.density) { 56.dp.toPx() }
 
+    /** The [OverscrollEffect] that should consume any overscroll on this draggable. */
+    internal val overscrollEffect: OverscrollEffect = DelegatingOverscrollEffect()
+
+    override fun shouldStartDrag(change: PointerInputChange): Boolean {
+        return layoutImpl.swipeDetector.detectSwipe(change)
+    }
+
+    override fun shouldConsumeNestedScroll(sign: Float): Boolean {
+        return this.enabled()
+    }
+
     override fun onDragStarted(
-        pointersDown: PointersInfo.PointersDown?,
-        overSlop: Float,
-    ): DragController {
-        check(overSlop != 0f)
-        val swipes = computeSwipes(pointersDown)
+        position: Offset,
+        sign: Float,
+        pointersDown: Int,
+        pointerType: PointerType?,
+    ): NestedDraggable.Controller {
+        check(sign != 0f)
+        val swipes = computeSwipes(position, pointersDown, pointerType)
         val fromContent = layoutImpl.contentForUserActions()
 
         swipes.updateSwipesResults(fromContent)
+        val upOrLeft = swipes.upOrLeftResult
+        val downOrRight = swipes.downOrRightResult
         val result =
-            (if (overSlop < 0f) swipes.upOrLeftResult else swipes.downOrRightResult)
-                // As we were unable to locate a valid target scene, the initial SwipeAnimation
-                // cannot be defined. Consequently, a simple NoOp Controller will be returned.
-                ?: return NoOpDragController
+            when {
+                sign < 0 -> upOrLeft ?: downOrRight
+                sign >= 0f -> downOrRight ?: upOrLeft
+                else -> null
+            } ?: return NoOpDragController
 
         val swipeAnimation = createSwipeAnimation(swipes, result)
         return updateDragController(swipes, swipeAnimation)
@@ -143,20 +126,109 @@
         )
     }
 
-    private fun computeSwipes(pointersDown: PointersInfo.PointersDown?): Swipes {
-        val fromSource = pointersDown?.let { resolveSwipeSource(it.startedPosition) }
+    private fun computeSwipes(
+        position: Offset,
+        pointersDown: Int,
+        pointerType: PointerType?,
+    ): Swipes {
+        val fromSource = resolveSwipeSource(position)
         return Swipes(
-            upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersDown, fromSource),
-            downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersDown, fromSource),
+            upOrLeft =
+                resolveSwipe(orientation, isUpOrLeft = true, fromSource, pointersDown, pointerType),
+            downOrRight =
+                resolveSwipe(orientation, isUpOrLeft = false, fromSource, pointersDown, pointerType),
         )
     }
+
+    /**
+     * An implementation of [OverscrollEffect] that delegates to the correct content effect
+     * depending on the current scene/overlays and transition.
+     */
+    private inner class DelegatingOverscrollEffect :
+        OverscrollEffect, SpaceVectorConverter by SpaceVectorConverter(orientation) {
+        private var currentContent: ContentKey? = null
+        private var currentDelegate: GestureEffect? = null
+            set(value) {
+                field?.let { delegate ->
+                    if (delegate.isInProgress) {
+                        layoutImpl.animationScope.launch { delegate.ensureApplyToFlingIsCalled() }
+                    }
+                }
+
+                field = value
+            }
+
+        override val isInProgress: Boolean
+            get() = currentDelegate?.isInProgress ?: false
+
+        override fun applyToScroll(
+            delta: Offset,
+            source: NestedScrollSource,
+            performScroll: (Offset) -> Offset,
+        ): Offset {
+            val available = delta.toFloat()
+            if (available == 0f) {
+                return performScroll(delta)
+            }
+
+            ensureDelegateIsNotNull(available)
+            val delegate = checkNotNull(currentDelegate)
+            return if (delegate.node.node.isAttached) {
+                delegate.applyToScroll(delta, source, performScroll)
+            } else {
+                performScroll(delta)
+            }
+        }
+
+        override suspend fun applyToFling(
+            velocity: Velocity,
+            performFling: suspend (Velocity) -> Velocity,
+        ) {
+            val available = velocity.toFloat()
+            if (available != 0f && isDrivingTransition) {
+                ensureDelegateIsNotNull(available)
+            }
+
+            // Note: we set currentDelegate and currentContent to null before calling performFling,
+            // which can suspend and take a lot of time.
+            val delegate = currentDelegate
+            currentDelegate = null
+            currentContent = null
+
+            if (delegate != null && delegate.node.node.isAttached) {
+                delegate.applyToFling(velocity, performFling)
+            } else {
+                performFling(velocity)
+            }
+        }
+
+        private fun ensureDelegateIsNotNull(direction: Float) {
+            require(direction != 0f)
+            if (isInProgress) {
+                return
+            }
+
+            val content =
+                if (isDrivingTransition) {
+                    checkNotNull(dragController).swipeAnimation.contentByDirection(direction)
+                } else {
+                    layoutImpl.contentForUserActions().key
+                }
+
+            if (content != currentContent) {
+                currentContent = content
+                currentDelegate = gestureEffectProvider(content)
+            }
+        }
+    }
 }
 
 private fun resolveSwipe(
     orientation: Orientation,
     isUpOrLeft: Boolean,
-    pointersDown: PointersInfo.PointersDown?,
     fromSource: SwipeSource.Resolved?,
+    pointersDown: Int,
+    pointerType: PointerType?,
 ): Swipe.Resolved {
     return Swipe.Resolved(
         direction =
@@ -175,28 +247,22 @@
                         SwipeDirection.Resolved.Down
                     }
             },
-        // If the number of pointers is not specified, 1 is assumed.
-        pointerCount = pointersDown?.count ?: 1,
-        // Resolves the pointer type only if all pointers are of the same type.
-        pointersType = pointersDown?.countByType?.keys?.singleOrNull(),
+        pointerCount = pointersDown,
+        pointerType = pointerType,
         fromSource = fromSource,
     )
 }
 
 /** @param swipes The [Swipes] associated to the current gesture. */
 private class DragControllerImpl(
-    private val draggableHandler: DraggableHandlerImpl,
+    private val draggableHandler: DraggableHandler,
     val swipes: Swipes,
     var swipeAnimation: SwipeAnimation<*>,
-) : DragController, SpaceVectorConverter by SpaceVectorConverter(draggableHandler.orientation) {
+) :
+    NestedDraggable.Controller,
+    SpaceVectorConverter by SpaceVectorConverter(draggableHandler.orientation) {
     val layoutState = draggableHandler.layoutImpl.state
 
-    val overscrollableContent: OverscrollableContent =
-        when (draggableHandler.orientation) {
-            Orientation.Vertical -> draggableHandler.layoutImpl.verticalOverscrollableContent
-            Orientation.Horizontal -> draggableHandler.layoutImpl.horizontalOverscrollableContent
-        }
-
     /**
      * Whether this handle is active. If this returns false, calling [onDrag] and [onStop] will do
      * nothing.
@@ -231,57 +297,25 @@
         if (delta == 0f || !isDrivingTransition || initialAnimation.isAnimatingOffset()) {
             return 0f
         }
+
         // swipeAnimation can change during the gesture, we want to always use the initial reference
         // during the whole drag gesture.
-        return dragWithOverscroll(delta, animation = initialAnimation)
-    }
-
-    private fun <T : ContentKey> dragWithOverscroll(
-        delta: Float,
-        animation: SwipeAnimation<T>,
-    ): Float {
-        require(delta != 0f) { "delta should not be 0" }
-        var overscrollEffect = overscrollableContent.currentOverscrollEffect
-
-        // If we're already overscrolling, continue with the current effect for a smooth finish.
-        if (overscrollEffect == null || !overscrollEffect.isInProgress) {
-            // Otherwise, determine the target content (toContent or fromContent) for the new
-            // overscroll effect based on the gesture's direction.
-            val content = animation.contentByDirection(delta)
-            overscrollEffect = overscrollableContent.applyOverscrollEffectOn(content)
-        }
-
-        // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags.
-        if (!overscrollEffect.node.node.isAttached) {
-            return drag(delta, animation)
-        }
-
-        return overscrollEffect
-            .applyToScroll(
-                delta = delta.toOffset(),
-                source = NestedScrollSource.UserInput,
-                performScroll = {
-                    val preScrollAvailable = it.toFloat()
-                    drag(preScrollAvailable, animation).toOffset()
-                },
-            )
-            .toFloat()
+        return drag(delta, animation = initialAnimation)
     }
 
     private fun <T : ContentKey> drag(delta: Float, animation: SwipeAnimation<T>): Float {
-        if (delta == 0f) return 0f
-
         val distance = animation.distance()
         val previousOffset = animation.dragOffset
         val desiredOffset = previousOffset + delta
-        val desiredProgress = animation.computeProgress(desiredOffset)
 
         // Note: the distance could be negative if fromContent is above or to the left of toContent.
         val newOffset =
             when {
-                distance == DistanceUnspecified ||
-                    animation.contentTransition.isWithinProgressRange(desiredProgress) ->
-                    desiredOffset
+                distance == DistanceUnspecified -> {
+                    // Consume everything so that we don't overscroll, this will be coerced later
+                    // when the distance is defined.
+                    delta
+                }
                 distance > 0f -> desiredOffset.fastCoerceIn(0f, distance)
                 else -> desiredOffset.fastCoerceIn(distance, 0f)
             }
@@ -290,12 +324,8 @@
         return newOffset - previousOffset
     }
 
-    override suspend fun onStop(velocity: Float): Float {
-        // To ensure that any ongoing animation completes gracefully and avoids an undefined state,
-        // we execute the actual `onStop` logic in a non-cancellable context. This prevents the
-        // coroutine from being cancelled prematurely, which could interrupt the animation.
-        // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags.
-        return withContext(NonCancellable) { onStop(velocity, swipeAnimation) }
+    override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float {
+        return onStop(velocity, swipeAnimation, awaitFling)
     }
 
     private suspend fun <T : ContentKey> onStop(
@@ -306,6 +336,7 @@
         // callbacks (like onAnimationCompleted()) might incorrectly finish a new transition that
         // replaced this one.
         swipeAnimation: SwipeAnimation<T>,
+        awaitFling: suspend () -> Unit,
     ): Float {
         // The state was changed since the drag started; don't do anything.
         if (!isDrivingTransition || swipeAnimation.isAnimatingOffset()) {
@@ -337,33 +368,7 @@
                 fromContent
             }
 
-        val overscrollEffect = overscrollableContent.applyOverscrollEffectOn(targetContent)
-
-        // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags.
-        if (!overscrollEffect.node.node.isAttached) {
-            return swipeAnimation.animateOffset(velocity, targetContent)
-        }
-
-        val overscrollCompletable = CompletableDeferred<Unit>()
-        try {
-            overscrollEffect.applyToFling(
-                velocity = velocity.toVelocity(),
-                performFling = {
-                    val velocityLeft = it.toFloat()
-                    swipeAnimation
-                        .animateOffset(
-                            velocityLeft,
-                            targetContent,
-                            overscrollCompletable = overscrollCompletable,
-                        )
-                        .toVelocity()
-                },
-            )
-        } finally {
-            overscrollCompletable.complete(Unit)
-        }
-
-        return velocity
+        return swipeAnimation.animateOffset(velocity, targetContent, awaitFling = awaitFling)
     }
 
     /**
@@ -408,10 +413,6 @@
                 isCloserToTarget()
         }
     }
-
-    override fun onCancel() {
-        swipeAnimation.contentTransition.coroutineScope.launch { onStop(velocity = 0f) }
-    }
 }
 
 /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
@@ -453,15 +454,15 @@
                     (actionSwipe.fromSource != null &&
                         actionSwipe.fromSource != swipe.fromSource) ||
                     // The action requires a specific pointerType.
-                    (actionSwipe.pointersType != null &&
-                        actionSwipe.pointersType != swipe.pointersType)
+                    (actionSwipe.pointerType != null &&
+                        actionSwipe.pointerType != swipe.pointerType)
             ) {
                 // This action is not eligible.
                 return@forEach
             }
 
             val sameFromSource = actionSwipe.fromSource == swipe.fromSource
-            val samePointerType = actionSwipe.pointersType == swipe.pointersType
+            val samePointerType = actionSwipe.pointerType == swipe.pointerType
             // Prioritize actions with a perfect match.
             if (sameFromSource && samePointerType) {
                 return actionResult
@@ -496,82 +497,6 @@
     }
 }
 
-internal class NestedScrollHandlerImpl(
-    private val draggableHandler: DraggableHandlerImpl,
-    private val pointersInfoOwner: PointersInfoOwner,
-) {
-    val connection: PriorityNestedScrollConnection = nestedScrollConnection()
-
-    private fun nestedScrollConnection(): PriorityNestedScrollConnection {
-        var lastPointersDown: PointersInfo.PointersDown? = null
-
-        return PriorityNestedScrollConnection(
-            orientation = draggableHandler.orientation,
-            canStartPreScroll = { _, _, _ -> false },
-            canStartPostScroll = { offsetAvailable, _, _ ->
-                if (offsetAvailable == 0f) return@PriorityNestedScrollConnection false
-
-                lastPointersDown =
-                    when (val info = pointersInfoOwner.pointersInfo()) {
-                        PointersInfo.MouseWheel -> {
-                            // Do not support mouse wheel interactions
-                            return@PriorityNestedScrollConnection false
-                        }
-
-                        is PointersInfo.PointersDown -> info
-                        null -> null
-                    }
-
-                draggableHandler.layoutImpl
-                    .contentForUserActions()
-                    .shouldEnableSwipes(draggableHandler.orientation)
-            },
-            onStart = { firstScroll ->
-                scrollController(
-                    dragController =
-                        draggableHandler.onDragStarted(
-                            pointersDown = lastPointersDown,
-                            overSlop = firstScroll,
-                        ),
-                    pointersInfoOwner = pointersInfoOwner,
-                )
-            },
-        )
-    }
-}
-
-private fun scrollController(
-    dragController: DragController,
-    pointersInfoOwner: PointersInfoOwner,
-): ScrollController {
-    return object : ScrollController {
-        override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
-            if (pointersInfoOwner.pointersInfo() == PointersInfo.MouseWheel) {
-                // Do not support mouse wheel interactions
-                return 0f
-            }
-
-            return dragController.onDrag(delta = deltaScroll)
-        }
-
-        override suspend fun OnStopScope.onStop(initialVelocity: Float): Float {
-            return dragController.onStop(velocity = initialVelocity)
-        }
-
-        override fun onCancel() {
-            dragController.onCancel()
-        }
-
-        /**
-         * We need to maintain scroll priority even if the scene transition can no longer consume
-         * the scroll gesture to allow us to return to the previous scene.
-         */
-        override fun canCancelScroll(available: Float, consumed: Float) = false
-
-        override fun canStopOnPreFling() = true
-    }
-}
-
 /**
  * The number of pixels below which there won't be a visible difference in the transition and from
  * which the animation can stop.
@@ -580,12 +505,8 @@
 // account instead.
 internal const val OffsetVisibilityThreshold = 0.5f
 
-private object NoOpDragController : DragController {
+private object NoOpDragController : NestedDraggable.Controller {
     override fun onDrag(delta: Float) = 0f
 
-    override suspend fun onStop(velocity: Float) = 0f
-
-    override fun onCancel() {
-        /* do nothing */
-    }
+    override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float = 0f
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
deleted file mode 100644
index 89320f13..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ /dev/null
@@ -1,678 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.animation.scene
-
-import androidx.annotation.VisibleForTesting
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
-import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
-import androidx.compose.runtime.Stable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.PointerId
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.PointerType
-import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.input.pointer.util.addPointerInputChange
-import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.PointerInputModifierNode
-import androidx.compose.ui.node.currentValueOf
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastFilter
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastSumBy
-import com.android.compose.ui.util.SpaceVectorConverter
-import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.sign
-import kotlinx.coroutines.currentCoroutineContext
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
-
-/**
- * Make an element draggable in the given [orientation].
- *
- * The main difference with [multiPointerDraggable] and
- * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number
- * of pointers that are down when the drag is started. If you don't need this information, you
- * should use `draggable` instead.
- *
- * Note that the current implementation is trivial: we wait for the touch slope on the *first* down
- * pointer, then we count the number of distinct pointers that are down right before calling
- * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not
- * dragged) and a second pointer is down and dragged. This is an implementation detail that might
- * change in the future.
- */
-@VisibleForTesting
-@Stable
-internal fun Modifier.multiPointerDraggable(
-    orientation: Orientation,
-    onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
-    onFirstPointerDown: () -> Unit = {},
-    swipeDetector: SwipeDetector = DefaultSwipeDetector,
-    dispatcher: NestedScrollDispatcher,
-): Modifier =
-    this.then(
-        MultiPointerDraggableElement(
-            orientation,
-            onDragStarted,
-            onFirstPointerDown,
-            swipeDetector,
-            dispatcher,
-        )
-    )
-
-private data class MultiPointerDraggableElement(
-    private val orientation: Orientation,
-    private val onDragStarted:
-        (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
-    private val onFirstPointerDown: () -> Unit,
-    private val swipeDetector: SwipeDetector,
-    private val dispatcher: NestedScrollDispatcher,
-) : ModifierNodeElement<MultiPointerDraggableNode>() {
-    override fun create(): MultiPointerDraggableNode =
-        MultiPointerDraggableNode(
-            orientation = orientation,
-            onDragStarted = onDragStarted,
-            onFirstPointerDown = onFirstPointerDown,
-            swipeDetector = swipeDetector,
-            dispatcher = dispatcher,
-        )
-
-    override fun update(node: MultiPointerDraggableNode) {
-        node.orientation = orientation
-        node.onDragStarted = onDragStarted
-        node.onFirstPointerDown = onFirstPointerDown
-        node.swipeDetector = swipeDetector
-    }
-}
-
-internal class MultiPointerDraggableNode(
-    orientation: Orientation,
-    var onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
-    var onFirstPointerDown: () -> Unit,
-    swipeDetector: SwipeDetector = DefaultSwipeDetector,
-    private val dispatcher: NestedScrollDispatcher,
-) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
-    private val pointerTracker = delegate(SuspendingPointerInputModifierNode { pointerTracker() })
-    private val pointerInput = delegate(SuspendingPointerInputModifierNode { pointerInput() })
-    private val velocityTracker = VelocityTracker()
-
-    var swipeDetector: SwipeDetector = swipeDetector
-        set(value) {
-            if (value != field) {
-                field = value
-                pointerInput.resetPointerInputHandler()
-            }
-        }
-
-    private var converter = SpaceVectorConverter(orientation)
-
-    fun Offset.toFloat(): Float = with(converter) { this@toFloat.toFloat() }
-
-    fun Velocity.toFloat(): Float = with(converter) { this@toFloat.toFloat() }
-
-    fun Float.toOffset(): Offset = with(converter) { this@toOffset.toOffset() }
-
-    fun Float.toVelocity(): Velocity = with(converter) { this@toVelocity.toVelocity() }
-
-    var orientation: Orientation = orientation
-        set(value) {
-            // Reset the pointer input whenever orientation changed.
-            if (value != field) {
-                field = value
-                converter = SpaceVectorConverter(value)
-                pointerInput.resetPointerInputHandler()
-            }
-        }
-
-    override fun onCancelPointerInput() {
-        pointerTracker.onCancelPointerInput()
-        pointerInput.onCancelPointerInput()
-    }
-
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize,
-    ) {
-        // The order is important here: the tracker is always called first.
-        pointerTracker.onPointerEvent(pointerEvent, pass, bounds)
-        pointerInput.onPointerEvent(pointerEvent, pass, bounds)
-    }
-
-    private var lastPointerEvent: PointerEvent? = null
-    private var startedPosition: Offset? = null
-    private var countPointersDown: Int = 0
-
-    internal fun pointersInfo(): PointersInfo? {
-        // This may be null, i.e. when the user uses TalkBack
-        val lastPointerEvent = lastPointerEvent ?: return null
-
-        if (lastPointerEvent.type == PointerEventType.Scroll) return PointersInfo.MouseWheel
-
-        val startedPosition = startedPosition ?: return null
-
-        return PointersInfo.PointersDown(
-            startedPosition = startedPosition,
-            count = countPointersDown,
-            countByType =
-                buildMap {
-                    lastPointerEvent.changes.fastForEach { change ->
-                        if (!change.pressed) return@fastForEach
-                        val newValue = (get(change.type) ?: 0) + 1
-                        put(change.type, newValue)
-                    }
-                },
-        )
-    }
-
-    private suspend fun PointerInputScope.pointerTracker() {
-        val currentContext = currentCoroutineContext()
-        awaitPointerEventScope {
-            var velocityPointerId: PointerId? = null
-            // Intercepts pointer inputs and exposes [PointersInfo], via
-            // [requireAncestorPointersInfoOwner], to our descendants.
-            while (currentContext.isActive) {
-                // During the Initial pass, we receive the event after our ancestors.
-                val pointerEvent = awaitPointerEvent(PointerEventPass.Initial)
-
-                // Ignore cursor has entered the input region.
-                // This will only be sent after the cursor is hovering when in the input region.
-                if (pointerEvent.type == PointerEventType.Enter) continue
-
-                val changes = pointerEvent.changes
-                lastPointerEvent = pointerEvent
-                countPointersDown = changes.countDown()
-
-                when {
-                    // There are no more pointers down.
-                    countPointersDown == 0 -> {
-                        startedPosition = null
-
-                        // In case of multiple events with 0 pointers down (not pressed) we may have
-                        // already removed the velocityPointer
-                        val lastPointerUp = changes.fastFilter { it.id == velocityPointerId }
-                        check(lastPointerUp.isEmpty() || lastPointerUp.size == 1) {
-                            "There are ${lastPointerUp.size} pointers up: $lastPointerUp"
-                        }
-                        if (lastPointerUp.size == 1) {
-                            velocityTracker.addPointerInputChange(lastPointerUp.first())
-                        }
-                    }
-
-                    // The first pointer down, startedPosition was not set.
-                    startedPosition == null -> {
-                        // Mouse wheel could start with multiple pointer down
-                        val firstPointerDown = changes.first()
-                        velocityPointerId = firstPointerDown.id
-                        velocityTracker.resetTracking()
-                        velocityTracker.addPointerInputChange(firstPointerDown)
-                        startedPosition = firstPointerDown.position
-                        onFirstPointerDown()
-                    }
-
-                    // Changes with at least one pointer
-                    else -> {
-                        val pointerChange = changes.first()
-
-                        // Assuming that the list of changes doesn't have two changes with the same
-                        // id (PointerId), we can check:
-                        // - If the first change has `id` equals to `velocityPointerId` (this should
-                        //   always be true unless the pointer has been removed).
-                        // - If it does, we've found our change event (assuming there aren't any
-                        //   others changes with the same id in this PointerEvent - not checked).
-                        // - If it doesn't, we can check that the change with that id isn't in first
-                        //   place (which should never happen - this will crash).
-                        check(
-                            pointerChange.id == velocityPointerId ||
-                                !changes.fastAny { it.id == velocityPointerId }
-                        ) {
-                            "$velocityPointerId is present, but not the first: $changes"
-                        }
-
-                        // If the previous pointer has been removed, we use the first available
-                        // change to keep tracking the velocity.
-                        velocityPointerId =
-                            if (pointerChange.pressed) {
-                                pointerChange.id
-                            } else {
-                                changes.first { it.pressed }.id
-                            }
-
-                        velocityTracker.addPointerInputChange(pointerChange)
-                    }
-                }
-            }
-        }
-    }
-
-    private suspend fun PointerInputScope.pointerInput() {
-        val currentContext = currentCoroutineContext()
-        awaitPointerEventScope {
-            while (currentContext.isActive) {
-                try {
-                    detectDragGestures(
-                        orientation = orientation,
-                        onDragStart = { pointersDown, overSlop ->
-                            onDragStarted(pointersDown, overSlop)
-                        },
-                        onDrag = { controller, amount ->
-                            dispatchScrollEvents(
-                                availableOnPreScroll = amount,
-                                onScroll = { controller.onDrag(it) },
-                                source = NestedScrollSource.UserInput,
-                            )
-                        },
-                        onDragEnd = { controller ->
-                            startFlingGesture(
-                                initialVelocity =
-                                    currentValueOf(LocalViewConfiguration)
-                                        .maximumFlingVelocity
-                                        .let {
-                                            val maxVelocity = Velocity(it, it)
-                                            velocityTracker.calculateVelocity(maxVelocity)
-                                        }
-                                        .toFloat(),
-                                onFling = { controller.onStop(it) },
-                            )
-                        },
-                        onDragCancel = { controller ->
-                            startFlingGesture(
-                                initialVelocity = 0f,
-                                onFling = { controller.onStop(it) },
-                            )
-                        },
-                        swipeDetector = swipeDetector,
-                    )
-                } catch (exception: CancellationException) {
-                    // If the coroutine scope is active, we can just restart the drag cycle.
-                    if (!currentContext.isActive) {
-                        throw exception
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Start a fling gesture in another CoroutineScope, this is to ensure that even when the pointer
-     * input scope is reset we will continue any coroutine scope that we started from these methods
-     * while the pointer input scope was active.
-     *
-     * Note: Inspired by [androidx.compose.foundation.gestures.ScrollableNode.onDragStopped]
-     */
-    private fun startFlingGesture(
-        initialVelocity: Float,
-        onFling: suspend (velocity: Float) -> Float,
-    ) {
-        // Note: [AwaitPointerEventScope] is annotated as @RestrictsSuspension, we need another
-        // CoroutineScope to run the fling gestures.
-        // We do not need to cancel this [Job], the source will take care of emitting an
-        // [onPostFling] before starting a new gesture.
-        dispatcher.coroutineScope.launch {
-            dispatchFlingEvents(availableOnPreFling = initialVelocity, onFling = onFling)
-        }
-    }
-
-    /**
-     * Use the nested scroll system to fire scroll events. This allows us to consume events from our
-     * ancestors during the pre-scroll and post-scroll phases.
-     *
-     * @param availableOnPreScroll amount available before the scroll, this can be partially
-     *   consumed by our ancestors.
-     * @param onScroll function that returns the amount consumed during a scroll given the amount
-     *   available after the [NestedScrollConnection.onPreScroll].
-     * @param source the source of the scroll event
-     * @return Total offset consumed.
-     */
-    private inline fun dispatchScrollEvents(
-        availableOnPreScroll: Float,
-        onScroll: (delta: Float) -> Float,
-        source: NestedScrollSource,
-    ): Float {
-        // PreScroll phase
-        val consumedByPreScroll =
-            dispatcher
-                .dispatchPreScroll(available = availableOnPreScroll.toOffset(), source = source)
-                .toFloat()
-
-        // Scroll phase
-        val availableOnScroll = availableOnPreScroll - consumedByPreScroll
-        val consumedBySelfScroll = onScroll(availableOnScroll)
-
-        // PostScroll phase
-        val availableOnPostScroll = availableOnScroll - consumedBySelfScroll
-        val consumedByPostScroll =
-            dispatcher
-                .dispatchPostScroll(
-                    consumed = consumedBySelfScroll.toOffset(),
-                    available = availableOnPostScroll.toOffset(),
-                    source = source,
-                )
-                .toFloat()
-
-        return consumedByPreScroll + consumedBySelfScroll + consumedByPostScroll
-    }
-
-    /**
-     * Use the nested scroll system to fire fling events. This allows us to consume events from our
-     * ancestors during the pre-fling and post-fling phases.
-     *
-     * @param availableOnPreFling velocity available before the fling, this can be partially
-     *   consumed by our ancestors.
-     * @param onFling function that returns the velocity consumed during the fling given the
-     *   velocity available after the [NestedScrollConnection.onPreFling].
-     * @return Total velocity consumed.
-     */
-    private suspend inline fun dispatchFlingEvents(
-        availableOnPreFling: Float,
-        onFling: suspend (velocity: Float) -> Float,
-    ): Float {
-        // PreFling phase
-        val consumedByPreFling =
-            dispatcher.dispatchPreFling(available = availableOnPreFling.toVelocity()).toFloat()
-
-        // Fling phase
-        val availableOnFling = availableOnPreFling - consumedByPreFling
-        val consumedBySelfFling = onFling(availableOnFling)
-
-        // PostFling phase
-        val availableOnPostFling = availableOnFling - consumedBySelfFling
-        val consumedByPostFling =
-            dispatcher
-                .dispatchPostFling(
-                    consumed = consumedBySelfFling.toVelocity(),
-                    available = availableOnPostFling.toVelocity(),
-                )
-                .toFloat()
-
-        return consumedByPreFling + consumedBySelfFling + consumedByPostFling
-    }
-
-    /**
-     * Detect drag gestures in the given [orientation].
-     *
-     * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and
-     * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for passing
-     * the number of pointers down to [onDragStart].
-     */
-    private suspend fun AwaitPointerEventScope.detectDragGestures(
-        orientation: Orientation,
-        onDragStart: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
-        onDrag: (controller: DragController, dragAmount: Float) -> Unit,
-        onDragEnd: (controller: DragController) -> Unit,
-        onDragCancel: (controller: DragController) -> Unit,
-        swipeDetector: SwipeDetector,
-    ) {
-        val consumablePointer =
-            awaitConsumableEvent {
-                    // We are searching for an event that can be used as the starting point for the
-                    // drag gesture. Our options are:
-                    // - Initial: These events should never be consumed by the MultiPointerDraggable
-                    //   since our ancestors can consume the gesture, but we would eliminate this
-                    //   possibility for our descendants.
-                    // - Main: These events are consumed during the drag gesture, and they are a
-                    //   good place to start if the previous event has not been consumed.
-                    // - Final: If the previous event has been consumed, we can wait for the Main
-                    //   pass to finish. If none of our ancestors were interested in the event, we
-                    //   can wait for an unconsumed event in the Final pass.
-                    val previousConsumed = currentEvent.changes.fastAny { it.isConsumed }
-                    if (previousConsumed) PointerEventPass.Final else PointerEventPass.Main
-                }
-                .changes
-                .first()
-
-        var overSlop = 0f
-        val onSlopReached = { change: PointerInputChange, over: Float ->
-            if (swipeDetector.detectSwipe(change)) {
-                change.consume()
-                overSlop = over
-            }
-        }
-
-        // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once it
-        // is public.
-        val drag =
-            when (orientation) {
-                Orientation.Horizontal ->
-                    awaitHorizontalTouchSlopOrCancellation(consumablePointer.id, onSlopReached)
-                Orientation.Vertical ->
-                    awaitVerticalTouchSlopOrCancellation(consumablePointer.id, onSlopReached)
-            } ?: return
-
-        val lastPointersDown =
-            checkNotNull(pointersInfo()) {
-                "We should have pointers down, last event: $currentEvent"
-            }
-                as PointersInfo.PointersDown
-        // Make sure that overSlop is not 0f. This can happen when the user drags by exactly
-        // the touch slop. However, the overSlop we pass to onDragStarted() is used to
-        // compute the direction we are dragging in, so overSlop should never be 0f.
-        if (overSlop == 0f) {
-            // If the user drags in the opposite direction, the delta becomes zero because
-            // we return to the original point. Therefore, we should use the previous event
-            // to calculate the direction.
-            val delta = (drag.position - drag.previousPosition).toFloat()
-            check(delta != 0f) {
-                buildString {
-                    append("delta is equal to 0 ")
-                    append("touchSlop ${currentValueOf(LocalViewConfiguration).touchSlop} ")
-                    append("consumablePointer.position ${consumablePointer.position} ")
-                    append("drag.position ${drag.position} ")
-                    append("drag.previousPosition ${drag.previousPosition}")
-                }
-            }
-            overSlop = delta.sign
-        }
-
-        val controller = onDragStart(lastPointersDown, overSlop)
-        val successful: Boolean
-        try {
-            onDrag(controller, overSlop)
-
-            successful =
-                drag(
-                    initialPointerId = drag.id,
-                    hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f },
-                    onDrag = {
-                        onDrag(controller, it.positionChange().toFloat())
-                        it.consume()
-                    },
-                    onIgnoredEvent = {
-                        // We are still dragging an object, but this event is not of interest to the
-                        // caller.
-                        // This event will not trigger the onDrag event, but we will consume the
-                        // event to prevent another pointerInput from interrupting the current
-                        // gesture just because the event was ignored.
-                        it.consume()
-                    },
-                )
-        } catch (t: Throwable) {
-            onDragCancel(controller)
-            throw t
-        }
-
-        if (successful) {
-            onDragEnd(controller)
-        } else {
-            onDragCancel(controller)
-        }
-    }
-
-    private suspend fun AwaitPointerEventScope.awaitConsumableEvent(
-        pass: () -> PointerEventPass
-    ): PointerEvent {
-        fun canBeConsumed(changes: List<PointerInputChange>): Boolean {
-            // At least one pointer down AND
-            return changes.fastAny { it.pressed } &&
-                // All pointers must be either:
-                changes.fastAll {
-                    // A) unconsumed AND recently pressed
-                    it.changedToDown() ||
-                        // B) unconsumed AND in a new position (on the current axis)
-                        it.positionChange().toFloat() != 0f
-                }
-        }
-
-        var event: PointerEvent
-        do {
-            event = awaitPointerEvent(pass = pass())
-        } while (!canBeConsumed(event.changes))
-
-        // We found a consumable event in the Main pass
-        return event
-    }
-
-    /**
-     * Continues to read drag events until all pointers are up or the drag event is canceled. The
-     * initial pointer to use for driving the drag is [initialPointerId]. [hasDragged] passes the
-     * result whether a change was detected from the drag function or not.
-     *
-     * Whenever the pointer moves, if [hasDragged] returns true, [onDrag] is called; otherwise,
-     * [onIgnoredEvent] is called.
-     *
-     * @return true when gesture ended with all pointers up and false when the gesture was canceled.
-     *
-     * Note: Inspired by DragGestureDetector.kt
-     */
-    private suspend inline fun AwaitPointerEventScope.drag(
-        initialPointerId: PointerId,
-        hasDragged: (PointerInputChange) -> Boolean,
-        onDrag: (PointerInputChange) -> Unit,
-        onIgnoredEvent: (PointerInputChange) -> Unit,
-    ): Boolean {
-        val pointer = currentEvent.changes.fastFirstOrNull { it.id == initialPointerId }
-        val isPointerUp = pointer?.pressed != true
-        if (isPointerUp) {
-            return false // The pointer has already been lifted, so the gesture is canceled
-        }
-        var pointerId = initialPointerId
-        while (true) {
-            val change = awaitDragOrUp(pointerId, hasDragged, onIgnoredEvent) ?: return false
-
-            if (change.isConsumed) {
-                return false
-            }
-
-            if (change.changedToUpIgnoreConsumed()) {
-                return true
-            }
-
-            onDrag(change)
-            pointerId = change.id
-        }
-    }
-
-    /**
-     * Waits for a single drag in one axis, final pointer up, or all pointers are up. When
-     * [initialPointerId] has lifted, another pointer that is down is chosen to be the finger
-     * governing the drag. When the final pointer is lifted, that [PointerInputChange] is returned.
-     * When a drag is detected, that [PointerInputChange] is returned. A drag is only detected when
-     * [hasDragged] returns `true`. Events that should not be captured are passed to
-     * [onIgnoredEvent].
-     *
-     * `null` is returned if there was an error in the pointer input stream and the pointer that was
-     * down was dropped before the 'up' was received.
-     *
-     * Note: Inspired by DragGestureDetector.kt
-     */
-    private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
-        initialPointerId: PointerId,
-        hasDragged: (PointerInputChange) -> Boolean,
-        onIgnoredEvent: (PointerInputChange) -> Unit,
-    ): PointerInputChange? {
-        var pointerId = initialPointerId
-        while (true) {
-            val event = awaitPointerEvent()
-            val dragEvent = event.changes.fastFirstOrNull { it.id == pointerId } ?: return null
-            if (dragEvent.changedToUpIgnoreConsumed()) {
-                val otherDown = event.changes.fastFirstOrNull { it.pressed }
-                if (otherDown == null) {
-                    // This is the last "up"
-                    return dragEvent
-                } else {
-                    pointerId = otherDown.id
-                }
-            } else if (hasDragged(dragEvent)) {
-                return dragEvent
-            } else {
-                onIgnoredEvent(dragEvent)
-            }
-        }
-    }
-
-    private fun List<PointerInputChange>.countDown() = fastSumBy { if (it.pressed) 1 else 0 }
-}
-
-internal fun interface PointersInfoOwner {
-    /**
-     * Provides information about the pointers interacting with this composable.
-     *
-     * @return A [PointersInfo] object containing details about the pointers, including the starting
-     *   position and the number of pointers down, or `null` if there are no pointers down.
-     */
-    fun pointersInfo(): PointersInfo?
-}
-
-internal sealed interface PointersInfo {
-    /**
-     * Holds information about pointer interactions within a composable.
-     *
-     * This class stores details such as the starting position of a gesture, the number of pointers
-     * down, and whether the last pointer event was a mouse wheel scroll.
-     *
-     * @param startedPosition The starting position of the gesture. This is the position where the
-     *   first pointer touched the screen, not necessarily the point where dragging begins. This may
-     *   be different from the initial touch position if a child composable intercepts the gesture
-     *   before this one.
-     * @param count The number of pointers currently down.
-     * @param countByType Provide a map of pointer types to the count of pointers of that type
-     *   currently down/pressed.
-     */
-    data class PointersDown(
-        val startedPosition: Offset,
-        val count: Int,
-        val countByType: Map<PointerType, Int>,
-    ) : PointersInfo {
-        init {
-            check(count > 0) { "We should have at least 1 pointer down, $count instead" }
-        }
-    }
-
-    /** Indicates whether the last pointer event was a mouse wheel scroll. */
-    data object MouseWheel : PointersInfo
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index de428a7..4e38947 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -457,7 +457,7 @@
 private constructor(
     val direction: SwipeDirection,
     val pointerCount: Int = 1,
-    val pointersType: PointerType? = null,
+    val pointerType: PointerType? = null,
     val fromSource: SwipeSource? = null,
 ) : UserAction() {
     companion object {
@@ -470,46 +470,46 @@
 
         fun Left(
             pointerCount: Int = 1,
-            pointersType: PointerType? = null,
+            pointerType: PointerType? = null,
             fromSource: SwipeSource? = null,
-        ) = Swipe(SwipeDirection.Left, pointerCount, pointersType, fromSource)
+        ) = Swipe(SwipeDirection.Left, pointerCount, pointerType, fromSource)
 
         fun Up(
             pointerCount: Int = 1,
-            pointersType: PointerType? = null,
+            pointerType: PointerType? = null,
             fromSource: SwipeSource? = null,
-        ) = Swipe(SwipeDirection.Up, pointerCount, pointersType, fromSource)
+        ) = Swipe(SwipeDirection.Up, pointerCount, pointerType, fromSource)
 
         fun Right(
             pointerCount: Int = 1,
-            pointersType: PointerType? = null,
+            pointerType: PointerType? = null,
             fromSource: SwipeSource? = null,
-        ) = Swipe(SwipeDirection.Right, pointerCount, pointersType, fromSource)
+        ) = Swipe(SwipeDirection.Right, pointerCount, pointerType, fromSource)
 
         fun Down(
             pointerCount: Int = 1,
-            pointersType: PointerType? = null,
+            pointerType: PointerType? = null,
             fromSource: SwipeSource? = null,
-        ) = Swipe(SwipeDirection.Down, pointerCount, pointersType, fromSource)
+        ) = Swipe(SwipeDirection.Down, pointerCount, pointerType, fromSource)
 
         fun Start(
             pointerCount: Int = 1,
-            pointersType: PointerType? = null,
+            pointerType: PointerType? = null,
             fromSource: SwipeSource? = null,
-        ) = Swipe(SwipeDirection.Start, pointerCount, pointersType, fromSource)
+        ) = Swipe(SwipeDirection.Start, pointerCount, pointerType, fromSource)
 
         fun End(
             pointerCount: Int = 1,
-            pointersType: PointerType? = null,
+            pointerType: PointerType? = null,
             fromSource: SwipeSource? = null,
-        ) = Swipe(SwipeDirection.End, pointerCount, pointersType, fromSource)
+        ) = Swipe(SwipeDirection.End, pointerCount, pointerType, fromSource)
     }
 
     override fun resolve(layoutDirection: LayoutDirection): UserAction.Resolved {
         return Resolved(
             direction = direction.resolve(layoutDirection),
             pointerCount = pointerCount,
-            pointersType = pointersType,
+            pointerType = pointerType,
             fromSource = fromSource?.resolve(layoutDirection),
         )
     }
@@ -519,7 +519,7 @@
         val direction: SwipeDirection.Resolved,
         val pointerCount: Int,
         val fromSource: SwipeSource.Resolved?,
-        val pointersType: PointerType?,
+        val pointerType: PointerType?,
     ) : UserAction.Resolved()
 }
 
@@ -724,6 +724,7 @@
                 density = density,
                 layoutDirection = layoutDirection,
                 swipeSourceDetector = swipeSourceDetector,
+                swipeDetector = swipeDetector,
                 transitionInterceptionThreshold = transitionInterceptionThreshold,
                 builder = builder,
                 animationScope = animationScope,
@@ -767,8 +768,9 @@
         layoutImpl.density = density
         layoutImpl.layoutDirection = layoutDirection
         layoutImpl.swipeSourceDetector = swipeSourceDetector
+        layoutImpl.swipeDetector = swipeDetector
         layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
     }
 
-    layoutImpl.Content(modifier, swipeDetector)
+    layoutImpl.Content(modifier)
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index e5bdc92..b4c449d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -31,6 +31,9 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.ApproachLayoutModifierNode
 import androidx.compose.ui.layout.ApproachMeasureScope
 import androidx.compose.ui.layout.LookaheadScope
@@ -49,10 +52,8 @@
 import com.android.compose.animation.scene.content.Overlay
 import com.android.compose.animation.scene.content.Scene
 import com.android.compose.animation.scene.content.state.TransitionState
-import com.android.compose.animation.scene.effect.GestureEffect
 import com.android.compose.ui.util.lerp
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
 
 /** The type for the content of movable elements. */
 internal typealias MovableElementContent = @Composable (@Composable () -> Unit) -> Unit
@@ -75,6 +76,7 @@
     internal var density: Density,
     internal var layoutDirection: LayoutDirection,
     internal var swipeSourceDetector: SwipeSourceDetector,
+    internal var swipeDetector: SwipeDetector,
     internal var transitionInterceptionThreshold: Float,
     builder: SceneTransitionLayoutScope.() -> Unit,
 
@@ -149,18 +151,6 @@
                     _movableContents = it
                 }
 
-    internal var horizontalOverscrollableContent =
-        OverscrollableContent(
-            animationScope = animationScope,
-            overscrollEffect = { content(it).scope.horizontalOverscrollGestureEffect },
-        )
-
-    internal var verticalOverscrollableContent =
-        OverscrollableContent(
-            animationScope = animationScope,
-            overscrollEffect = { content(it).scope.verticalOverscrollGestureEffect },
-        )
-
     /**
      * The different values of a shared value keyed by a a [ValueKey] and the different elements and
      * contents it is associated to.
@@ -175,8 +165,8 @@
                 }
 
     // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed.
-    internal val horizontalDraggableHandler: DraggableHandlerImpl
-    internal val verticalDraggableHandler: DraggableHandlerImpl
+    internal val horizontalDraggableHandler: DraggableHandler
+    internal val verticalDraggableHandler: DraggableHandler
 
     internal val elementStateScope = ElementStateScopeImpl(this)
     internal val propertyTransformationScope = PropertyTransformationScopeImpl(this)
@@ -190,16 +180,33 @@
 
     internal var lastSize: IntSize = IntSize.Zero
 
+    /**
+     * An empty [NestedScrollDispatcher] and [NestedScrollConnection]. These are composed above our
+     * [SwipeToSceneElement] modifiers, so that the dispatcher will be used by the nested draggables
+     * to launch fling events, making sure that they are not cancelled unless this whole layout is
+     * removed from composition.
+     */
+    private val nestedScrollDispatcher = NestedScrollDispatcher()
+    private val nestedScrollConnection = object : NestedScrollConnection {}
+
     init {
         updateContents(builder, layoutDirection)
 
         // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the
         // current scene (required for SwipeTransition).
         horizontalDraggableHandler =
-            DraggableHandlerImpl(layoutImpl = this, orientation = Orientation.Horizontal)
+            DraggableHandler(
+                layoutImpl = this,
+                orientation = Orientation.Horizontal,
+                gestureEffectProvider = { content(it).scope.horizontalOverscrollGestureEffect },
+            )
 
         verticalDraggableHandler =
-            DraggableHandlerImpl(layoutImpl = this, orientation = Orientation.Vertical)
+            DraggableHandler(
+                layoutImpl = this,
+                orientation = Orientation.Vertical,
+                gestureEffectProvider = { content(it).scope.verticalOverscrollGestureEffect },
+            )
 
         // Make sure that the state is created on the same thread (most probably the main thread)
         // than this STLImpl.
@@ -379,14 +386,15 @@
     }
 
     @Composable
-    internal fun Content(modifier: Modifier, swipeDetector: SwipeDetector) {
+    internal fun Content(modifier: Modifier) {
         Box(
             modifier
+                .nestedScroll(nestedScrollConnection, nestedScrollDispatcher)
                 // Handle horizontal and vertical swipes on this layout.
                 // Note: order here is important and will give a slight priority to the vertical
                 // swipes.
-                .swipeToScene(horizontalDraggableHandler, swipeDetector)
-                .swipeToScene(verticalDraggableHandler, swipeDetector)
+                .swipeToScene(horizontalDraggableHandler)
+                .swipeToScene(verticalDraggableHandler)
                 .then(LayoutElement(layoutImpl = this))
         ) {
             LookaheadScope {
@@ -580,23 +588,3 @@
         return layout(width, height) { placeable.place(0, 0) }
     }
 }
-
-internal class OverscrollableContent(
-    private val animationScope: CoroutineScope,
-    private val overscrollEffect: (ContentKey) -> GestureEffect,
-) {
-    private var currentContent: ContentKey? = null
-    var currentOverscrollEffect: GestureEffect? = null
-
-    fun applyOverscrollEffectOn(contentKey: ContentKey): GestureEffect {
-        if (currentContent == contentKey) return currentOverscrollEffect!!
-
-        currentOverscrollEffect?.apply { animationScope.launch { ensureApplyToFlingIsCalled() } }
-
-        // We are wrapping the overscroll effect.
-        val overscrollEffect = overscrollEffect(contentKey)
-        currentContent = contentKey
-        currentOverscrollEffect = overscrollEffect
-        return overscrollEffect
-    }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index ff8efc2..d50304d4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -34,7 +34,7 @@
 /** The transitions configuration of a [SceneTransitionLayout]. */
 class SceneTransitions
 internal constructor(
-    internal val defaultSwipeSpec: SpringSpec<Float>,
+    internal val defaultMotionSpatialSpec: SpringSpec<Float>,
     internal val transitionSpecs: List<TransitionSpecImpl>,
     internal val interruptionHandler: InterruptionHandler,
 ) {
@@ -132,7 +132,7 @@
 
         val Empty =
             SceneTransitions(
-                defaultSwipeSpec = DefaultSwipeSpec,
+                defaultMotionSpatialSpec = DefaultSwipeSpec,
                 transitionSpecs = emptyList(),
                 interruptionHandler = DefaultInterruptionHandler,
             )
@@ -194,9 +194,9 @@
      * The [SpringSpec] used to animate the associated transition progress when the transition was
      * started by a swipe and is now animating back to a scene because the user lifted their finger.
      *
-     * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used.
+     * If `null`, then the [SceneTransitions.defaultMotionSpatialSpec] will be used.
      */
-    val swipeSpec: SpringSpec<Float>?
+    val motionSpatialSpec: AnimationSpec<Float>?
 
     /**
      * The distance it takes for this transition to animate from 0% to 100% when it is driven by a
@@ -213,7 +213,7 @@
         internal val Empty =
             TransformationSpecImpl(
                 progressSpec = snap(),
-                swipeSpec = null,
+                motionSpatialSpec = null,
                 distance = null,
                 transformationMatchers = emptyList(),
             )
@@ -246,7 +246,7 @@
                 val reverse = transformationSpec.invoke(transition)
                 TransformationSpecImpl(
                     progressSpec = reverse.progressSpec,
-                    swipeSpec = reverse.swipeSpec,
+                    motionSpatialSpec = reverse.motionSpatialSpec,
                     distance = reverse.distance,
                     transformationMatchers =
                         reverse.transformationMatchers.map {
@@ -276,7 +276,7 @@
  */
 internal class TransformationSpecImpl(
     override val progressSpec: AnimationSpec<Float>,
-    override val swipeSpec: SpringSpec<Float>?,
+    override val motionSpatialSpec: SpringSpec<Float>?,
     override val distance: UserActionDistance?,
     override val transformationMatchers: List<TransformationMatcher>,
 ) : TransformationSpec {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index ba92f9b..b1d6d1e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.util.fastCoerceIn
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.content.state.TransitionState.Companion.DistanceUnspecified
 import kotlin.math.absoluteValue
@@ -76,7 +77,16 @@
             return DistanceUnspecified
         }
 
-        val distance = if (isUpOrLeft) -absoluteDistance else absoluteDistance
+        // Compute the signed distance and make sure that the offset is always coerced in the right
+        // range.
+        val distance =
+            if (isUpOrLeft) {
+                animation.dragOffset = animation.dragOffset.fastCoerceIn(-absoluteDistance, 0f)
+                -absoluteDistance
+            } else {
+                animation.dragOffset = animation.dragOffset.fastCoerceIn(0f, absoluteDistance)
+                absoluteDistance
+            }
         lastDistance = distance
         return distance
     }
@@ -294,12 +304,10 @@
         initialVelocity: Float,
         targetContent: T,
         spec: AnimationSpec<Float>? = null,
-        overscrollCompletable: CompletableDeferred<Unit>? = null,
+        awaitFling: (suspend () -> Unit)? = null,
     ): Float {
         check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" }
 
-        val initialProgress = progress
-
         val targetContent =
             if (targetContent != currentContent && !canChangeContent(targetContent)) {
                 currentContent
@@ -307,18 +315,16 @@
                 targetContent
             }
 
-        // Skip the animation if we have already reached the target content and the overscroll does
-        // not animate anything.
-        val hasReachedTargetContent =
-            (targetContent == toContent && initialProgress >= 1f) ||
-                (targetContent == fromContent && initialProgress <= 0f)
-        val skipAnimation =
-            hasReachedTargetContent && !contentTransition.isWithinProgressRange(initialProgress)
-
         val distance = distance()
-        check(distance != DistanceUnspecified) { "distance is equal to $DistanceUnspecified" }
-
-        val targetOffset = if (targetContent == fromContent) 0f else distance
+        val targetOffset =
+            if (targetContent == fromContent) {
+                0f
+            } else {
+                check(distance != DistanceUnspecified) {
+                    "distance is equal to $DistanceUnspecified"
+                }
+                distance
+            }
 
         // If the effective current content changed, it should be reflected right now in the
         // current state, even before the settle animation is ongoing. That way all the
@@ -350,33 +356,17 @@
 
         check(isAnimatingOffset())
 
-        // Note: we still create the animatable and set it on offsetAnimation even when
-        // skipAnimation is true, just so that isUserInputOngoing and isAnimatingOffset() are
-        // unchanged even despite this small skip-optimization (which is just an implementation
-        // detail).
-        if (skipAnimation) {
-            // Unblock the job.
-            offsetAnimationRunnable.complete {
-                // Wait for overscroll to finish so that the transition is removed from the STLState
-                // only after the overscroll is done, to avoid dropping frame right when the user
-                // lifts their finger and overscroll is animated to 0.
-                overscrollCompletable?.await()
-            }
-            return 0f
-        }
-
-        val swipeSpec =
+        val motionSpatialSpec =
             spec
-                ?: contentTransition.transformationSpec.swipeSpec
-                ?: layoutState.transitions.defaultSwipeSpec
+                ?: contentTransition.transformationSpec.motionSpatialSpec
+                ?: layoutState.transitions.defaultMotionSpatialSpec
 
         val velocityConsumed = CompletableDeferred<Float>()
-
         offsetAnimationRunnable.complete {
             val result =
                 animatable.animateTo(
                     targetValue = targetOffset,
-                    animationSpec = swipeSpec,
+                    animationSpec = motionSpatialSpec,
                     initialVelocity = initialVelocity,
                 )
 
@@ -385,9 +375,9 @@
             velocityConsumed.complete(initialVelocity - result.endState.velocity)
 
             // Wait for overscroll to finish so that the transition is removed from the STLState
-            // only after the overscroll is done, to avoid dropping frame right when the user
-            // lifts their finger and overscroll is animated to 0.
-            overscrollCompletable?.await()
+            // only after the overscroll is done, to avoid dropping frame right when the user lifts
+            // their finger and overscroll is animated to 0.
+            awaitFling?.invoke()
         }
 
         return velocityConsumed.await()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index e221211..19f707d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -19,153 +19,35 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
-import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.PointerInputModifierNode
-import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.content.Content
+import com.android.compose.gesture.nestedDraggable
 
 /**
  * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
  */
 @Stable
-internal fun Modifier.swipeToScene(
-    draggableHandler: DraggableHandlerImpl,
-    swipeDetector: SwipeDetector,
-): Modifier {
-    return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled()))
+internal fun Modifier.swipeToScene(draggableHandler: DraggableHandler): Modifier {
+    return this.nestedDraggable(
+        draggable = draggableHandler,
+        orientation = draggableHandler.orientation,
+        overscrollEffect = draggableHandler.overscrollEffect,
+        enabled = draggableHandler.enabled(),
+    )
 }
 
-private fun DraggableHandlerImpl.enabled(): Boolean {
+internal fun DraggableHandler.enabled(): Boolean {
     return isDrivingTransition || contentForSwipes().shouldEnableSwipes(orientation)
 }
 
-private fun DraggableHandlerImpl.contentForSwipes(): Content {
+private fun DraggableHandler.contentForSwipes(): Content {
     return layoutImpl.contentForUserActions()
 }
 
 /** Whether swipe should be enabled in the given [orientation]. */
-internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
+private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
     if (userActions.isEmpty() || !areSwipesAllowed()) {
         return false
     }
 
     return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
 }
-
-private data class SwipeToSceneElement(
-    val draggableHandler: DraggableHandlerImpl,
-    val swipeDetector: SwipeDetector,
-    val enabled: Boolean,
-) : ModifierNodeElement<SwipeToSceneRootNode>() {
-    override fun create(): SwipeToSceneRootNode =
-        SwipeToSceneRootNode(draggableHandler, swipeDetector, enabled)
-
-    override fun update(node: SwipeToSceneRootNode) {
-        node.update(draggableHandler, swipeDetector, enabled)
-    }
-}
-
-private class SwipeToSceneRootNode(
-    draggableHandler: DraggableHandlerImpl,
-    swipeDetector: SwipeDetector,
-    enabled: Boolean,
-) : DelegatingNode() {
-    private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null
-
-    fun update(
-        draggableHandler: DraggableHandlerImpl,
-        swipeDetector: SwipeDetector,
-        enabled: Boolean,
-    ) {
-        // Disabled.
-        if (!enabled) {
-            delegateNode?.let { undelegate(it) }
-            delegateNode = null
-            return
-        }
-
-        // Disabled => Enabled.
-        val nullableDelegate = delegateNode
-        if (nullableDelegate == null) {
-            delegateNode = create(draggableHandler, swipeDetector)
-            return
-        }
-
-        // Enabled => Enabled (update).
-        if (draggableHandler == nullableDelegate.draggableHandler) {
-            // Simple update, just update the swipe detector directly and keep the node.
-            nullableDelegate.swipeDetector = swipeDetector
-        } else {
-            // The draggableHandler changed, force recreate the underlying SwipeToSceneNode.
-            undelegate(nullableDelegate)
-            delegateNode = create(draggableHandler, swipeDetector)
-        }
-    }
-
-    private fun create(
-        draggableHandler: DraggableHandlerImpl,
-        swipeDetector: SwipeDetector,
-    ): SwipeToSceneNode {
-        return delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
-    }
-}
-
-private class SwipeToSceneNode(
-    val draggableHandler: DraggableHandlerImpl,
-    swipeDetector: SwipeDetector,
-) : DelegatingNode(), PointerInputModifierNode {
-    private val dispatcher = NestedScrollDispatcher()
-    private val multiPointerDraggableNode =
-        delegate(
-            MultiPointerDraggableNode(
-                orientation = draggableHandler.orientation,
-                onDragStarted = draggableHandler::onDragStarted,
-                onFirstPointerDown = ::onFirstPointerDown,
-                swipeDetector = swipeDetector,
-                dispatcher = dispatcher,
-            )
-        )
-
-    var swipeDetector: SwipeDetector
-        get() = multiPointerDraggableNode.swipeDetector
-        set(value) {
-            multiPointerDraggableNode.swipeDetector = value
-        }
-
-    private val nestedScrollHandlerImpl =
-        NestedScrollHandlerImpl(
-            draggableHandler = draggableHandler,
-            pointersInfoOwner = { multiPointerDraggableNode.pointersInfo() },
-        )
-
-    init {
-        delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher))
-    }
-
-    private fun onFirstPointerDown() {
-        // When we drag our finger across the screen, the NestedScrollConnection keeps track of all
-        // the scroll events until we lift our finger. However, in some cases, the connection might
-        // not receive the "up" event. This can lead to an incorrect initial state for the gesture.
-        // To prevent this issue, we can call the reset() method when the first finger touches the
-        // screen. This ensures that the NestedScrollConnection starts from a correct state.
-        nestedScrollHandlerImpl.connection.reset()
-    }
-
-    override fun onDetach() {
-        // Make sure we reset the scroll connection when this modifier is removed from composition
-        nestedScrollHandlerImpl.connection.reset()
-    }
-
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize,
-    ) = multiPointerDraggableNode.onPointerEvent(pointerEvent, pass, bounds)
-
-    override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 998054e..776d553 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -40,7 +40,7 @@
      * The default [AnimationSpec] used when after the user lifts their finger after starting a
      * swipe to transition, to animate back into one of the 2 scenes we are transitioning to.
      */
-    var defaultSwipeSpec: SpringSpec<Float>
+    var defaultMotionSpatialSpec: SpringSpec<Float>
 
     /**
      * The [InterruptionHandler] used when transitions are interrupted. Defaults to
@@ -145,9 +145,9 @@
      * The [SpringSpec] used to animate the associated transition progress when the transition was
      * started by a swipe and is now animating back to a scene because the user lifted their finger.
      *
-     * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used.
+     * If `null`, then the [SceneTransitionsBuilder.defaultMotionSpatialSpec] will be used.
      */
-    var swipeSpec: SpringSpec<Float>?
+    var motionSpatialSpec: SpringSpec<Float>?
 
     /** The CUJ associated to this transitions. */
     @CujType var cuj: Int?
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 7ca5215..9a9b05e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -41,11 +41,15 @@
 
 internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
     val impl = SceneTransitionsBuilderImpl().apply(builder)
-    return SceneTransitions(impl.defaultSwipeSpec, impl.transitionSpecs, impl.interruptionHandler)
+    return SceneTransitions(
+        defaultMotionSpatialSpec = impl.defaultMotionSpatialSpec,
+        transitionSpecs = impl.transitionSpecs,
+        interruptionHandler = impl.interruptionHandler,
+    )
 }
 
 private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
-    override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
+    override var defaultMotionSpatialSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
     override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler
 
     val transitionSpecs = mutableListOf<TransitionSpecImpl>()
@@ -105,7 +109,7 @@
             val impl = TransitionBuilderImpl(transition).apply(builder)
             return TransformationSpecImpl(
                 progressSpec = impl.spec,
-                swipeSpec = impl.swipeSpec,
+                motionSpatialSpec = impl.motionSpatialSpec,
                 distance = impl.distance,
                 transformationMatchers = impl.transformationMatchers,
             )
@@ -209,7 +213,7 @@
 internal class TransitionBuilderImpl(override val transition: TransitionState.Transition) :
     BaseTransitionBuilderImpl(), TransitionBuilder {
     override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
-    override var swipeSpec: SpringSpec<Float>? = null
+    override var motionSpatialSpec: SpringSpec<Float>? = null
     override var distance: UserActionDistance? = null
     override var cuj: Int? = null
     private val durationMillis: Int by lazy {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 712af56..f772f1a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -373,15 +373,6 @@
             }
         }
 
-        /**
-         * Checks if the given [progress] value is within the valid range for this transition.
-         *
-         * The valid range is between 0f and 1f, inclusive.
-         */
-        internal fun isWithinProgressRange(progress: Float): Boolean {
-            return progress >= 0f && progress <= 1f
-        }
-
         internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float {
             if (replacedTransition != null) {
                 return replacedTransition.interruptionProgress(layoutImpl)
@@ -390,10 +381,10 @@
             fun create(): Animatable<Float, AnimationVector1D> {
                 val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold)
                 layoutImpl.animationScope.launch {
-                    val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec
+                    val motionSpatialSpec = layoutImpl.state.transitions.defaultMotionSpatialSpec
                     val progressSpec =
                         spring(
-                            stiffness = swipeSpec.stiffness,
+                            stiffness = motionSpatialSpec.stiffness,
                             dampingRatio = Spring.DampingRatioNoBouncy,
                             visibilityThreshold = ProgressVisibilityThreshold,
                         )
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
index 7c4dbf1..00cd0ca 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
@@ -104,7 +104,7 @@
     val alphaSpec = spring<Float>(stiffness = 1200f, dampingRatio = 0.99f)
 
     // The spring animating the progress when releasing the finger.
-    swipeSpec =
+    motionSpatialSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             dampingRatio = Spring.DampingRatioNoBouncy,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index dbac62f..4a0c330 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -38,9 +38,11 @@
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.content.state.TransitionState.Transition
 import com.android.compose.animation.scene.subjects.assertThat
+import com.android.compose.gesture.NestedDraggable
 import com.android.compose.test.MonotonicClockTestScope
 import com.android.compose.test.runMonotonicClockTest
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.sign
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.async
@@ -51,18 +53,6 @@
 private const val SCREEN_SIZE = 100f
 private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt())
 
-private fun pointersDown(
-    startedPosition: Offset = Offset.Zero,
-    pointersDown: Int = 1,
-    pointersDownByType: Map<PointerType, Int> = mapOf(PointerType.Touch to pointersDown),
-): PointersInfo.PointersDown {
-    return PointersInfo.PointersDown(
-        startedPosition = startedPosition,
-        count = pointersDown,
-        countByType = pointersDownByType,
-    )
-}
-
 @RunWith(AndroidJUnit4::class)
 class DraggableHandlerTest {
     private class TestGestureScope(val testScope: MonotonicClockTestScope) {
@@ -123,6 +113,7 @@
                     density = Density(1f),
                     layoutDirection = LayoutDirection.Ltr,
                     swipeSourceDetector = DefaultEdgeDetector,
+                    swipeDetector = DefaultSwipeDetector,
                     transitionInterceptionThreshold = transitionInterceptionThreshold,
                     builder = scenesBuilder,
 
@@ -134,16 +125,6 @@
 
         val draggableHandler = layoutImpl.verticalDraggableHandler
         val horizontalDraggableHandler = layoutImpl.horizontalDraggableHandler
-
-        var pointerInfoOwner: () -> PointersInfo = { pointersDown() }
-
-        fun nestedScrollConnection() =
-            NestedScrollHandlerImpl(
-                    draggableHandler = draggableHandler,
-                    pointersInfoOwner = { pointerInfoOwner() },
-                )
-                .connection
-
         val velocityThreshold = draggableHandler.velocityThreshold
 
         fun down(fractionOfScreen: Float) =
@@ -211,41 +192,51 @@
         }
 
         fun onDragStarted(
-            pointersInfo: PointersInfo.PointersDown = pointersDown(),
             overSlop: Float,
+            position: Offset = Offset.Zero,
+            pointersDown: Int = 1,
+            pointerType: PointerType? = PointerType.Touch,
             expectedConsumedOverSlop: Float = overSlop,
-        ): DragController {
-            // overSlop should be 0f only if the drag gesture starts with startDragImmediately
-            if (overSlop == 0f) error("Consider using onDragStartedImmediately()")
+        ): NestedDraggable.Controller {
             return onDragStarted(
                 draggableHandler = draggableHandler,
-                pointersInfo = pointersInfo,
                 overSlop = overSlop,
+                position = position,
+                pointersDown = pointersDown,
+                pointerType = pointerType,
                 expectedConsumedOverSlop = expectedConsumedOverSlop,
             )
         }
 
         fun onDragStarted(
-            draggableHandler: DraggableHandler,
-            pointersInfo: PointersInfo.PointersDown = pointersDown(),
-            overSlop: Float = 0f,
+            draggableHandler: NestedDraggable,
+            overSlop: Float,
+            position: Offset = Offset.Zero,
+            pointersDown: Int = 1,
+            pointerType: PointerType? = PointerType.Touch,
             expectedConsumedOverSlop: Float = overSlop,
-        ): DragController {
-            val dragController =
-                draggableHandler.onDragStarted(pointersDown = pointersInfo, overSlop = overSlop)
+        ): NestedDraggable.Controller {
+            // overSlop should be 0f only if the drag gesture starts with startDragImmediately.
+            if (overSlop == 0f) error("Consider using onDragStartedImmediately()")
 
-            // MultiPointerDraggable will always call onDelta with the initial overSlop right after
+            val dragController =
+                draggableHandler.onDragStarted(position, overSlop.sign, pointersDown, pointerType)
+
+            // MultiPointerDraggable will always call onDelta with the initial overSlop right after.
             dragController.onDragDelta(pixels = overSlop, expectedConsumedOverSlop)
 
             return dragController
         }
 
-        fun DragController.onDragDelta(pixels: Float, expectedConsumed: Float = pixels) {
+        fun NestedDraggable.Controller.onDragDelta(
+            pixels: Float,
+            expectedConsumed: Float = pixels,
+        ) {
             val consumed = onDrag(delta = pixels)
             assertThat(consumed).isEqualTo(expectedConsumed)
         }
 
-        suspend fun DragController.onDragStoppedAnimateNow(
+        suspend fun NestedDraggable.Controller.onDragStoppedAnimateNow(
             velocity: Float,
             onAnimationStart: () -> Unit,
             onAnimationEnd: (Float) -> Unit,
@@ -255,7 +246,7 @@
             onAnimationEnd(velocityConsumed.await())
         }
 
-        suspend fun DragController.onDragStoppedAnimateNow(
+        suspend fun NestedDraggable.Controller.onDragStoppedAnimateNow(
             velocity: Float,
             onAnimationStart: () -> Unit,
         ) =
@@ -265,8 +256,8 @@
                 onAnimationEnd = {},
             )
 
-        fun DragController.onDragStoppedAnimateLater(velocity: Float): Deferred<Float> {
-            val velocityConsumed = testScope.async { onStop(velocity) }
+        fun NestedDraggable.Controller.onDragStoppedAnimateLater(velocity: Float): Deferred<Float> {
+            val velocityConsumed = testScope.async { onDragStopped(velocity, awaitFling = {}) }
             testScope.testScheduler.runCurrent()
             return velocityConsumed
         }
@@ -415,12 +406,13 @@
     }
 
     @Test
-    fun onDragIntoNoAction_stayIdle() = runGestureTest {
+    fun onDragIntoNoAction_overscrolls() = runGestureTest {
         navigateToSceneC()
 
-        // We are on SceneC which has no action in Down direction
+        // We are on SceneC which has no action in Down direction, we still start a transition so
+        // that we can overscroll on that scene.
         onDragStarted(overSlop = 10f, expectedConsumedOverSlop = 0f)
-        assertIdle(currentScene = SceneC)
+        assertTransition(fromScene = SceneC, toScene = SceneB, progress = 0f)
     }
 
     @Test
@@ -429,8 +421,7 @@
         mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB), Swipe.Down to SceneC)
         val dragController =
             onDragStarted(
-                pointersInfo =
-                    pointersDown(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f)),
+                position = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f),
                 overSlop = up(fractionOfScreen = 0.2f),
             )
         assertTransition(
@@ -454,7 +445,7 @@
 
         // Start dragging from the bottom
         onDragStarted(
-            pointersInfo = pointersDown(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)),
+            position = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE),
             overSlop = up(fractionOfScreen = 0.1f),
         )
         assertTransition(
@@ -555,8 +546,7 @@
         navigateToSceneC()
 
         // Swipe up from the middle to transition to scene B.
-        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
-        onDragStarted(pointersInfo = middle, overSlop = up(0.1f))
+        onDragStarted(position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f), overSlop = up(0.1f))
         assertTransition(fromScene = SceneC, toScene = SceneB, isUserInputOngoing = true)
 
         // Freeze the transition.
@@ -607,61 +597,13 @@
     }
 
     @Test
-    fun nestedScrollUseFromSourceInfo() = runGestureTest {
-        // Start at scene C.
-        navigateToSceneC()
-        val nestedScroll = nestedScrollConnection()
-
-        // Drag from the **top** of the screen
-        pointerInfoOwner = { pointersDown() }
-        assertIdle(currentScene = SceneC)
-
-        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
-        assertTransition(
-            currentScene = SceneC,
-            fromScene = SceneC,
-            // userAction: Swipe.Up to SceneB
-            toScene = SceneB,
-            progress = 0.1f,
-        )
-
-        // Reset to SceneC
-        nestedScroll.preFling(Velocity.Zero)
-        advanceUntilIdle()
-
-        // Drag from the **bottom** of the screen
-        pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) }
-        assertIdle(currentScene = SceneC)
-
-        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
-        assertTransition(
-            currentScene = SceneC,
-            fromScene = SceneC,
-            // userAction: Swipe.Up(fromSource = Edge.Bottom) to SceneA
-            toScene = SceneA,
-            progress = 0.1f,
-        )
-    }
-
-    @Test
-    fun ignoreMouseWheel() = runGestureTest {
-        // Start at scene C.
-        navigateToSceneC()
-        val nestedScroll = nestedScrollConnection()
-
-        // Use mouse wheel
-        pointerInfoOwner = { PointersInfo.MouseWheel }
-        assertIdle(currentScene = SceneC)
-
-        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
-        assertIdle(currentScene = SceneC)
-    }
-
-    @Test
     fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
         // Swipe up from the middle to transition to scene B.
-        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
-        val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.1f))
+        val dragController =
+            onDragStarted(
+                position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f),
+                overSlop = up(0.1f),
+            )
         assertTransition(fromScene = SceneA, toScene = SceneB, isUserInputOngoing = true)
 
         dragController.onDragStoppedAnimateLater(velocity = 0f)
@@ -671,8 +613,11 @@
     @Test
     fun emptyOverscrollAbortsSettleAnimationAndExposeTheConsumedVelocity() = runGestureTest {
         // Swipe up to scene B at progress = 200%.
-        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
-        val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.99f))
+        val dragController =
+            onDragStarted(
+                position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f),
+                overSlop = up(0.99f),
+            )
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.99f)
 
         // Release the finger.
@@ -689,34 +634,18 @@
     }
 
     @Test
-    fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() = runGestureTest {
-        val nestedScroll = nestedScrollConnection()
-
-        // Overscroll is disabled, it will scroll up to 100%
-        nestedScroll.scroll(available = upOffset(fractionOfScreen = 2f))
-        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
-
-        // We need to maintain scroll priority even if the scene transition can no longer consume
-        // the scroll gesture.
-        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
-        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
-
-        // A scroll gesture in the opposite direction allows us to return to the previous scene.
-        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.5f))
-        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.5f)
-    }
-
-    @Test
     fun overscroll_releaseBetween0And100Percent_up() = runGestureTest {
         // Make scene B overscrollable.
         layoutState.transitions = transitions {
-            defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
+            defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
             from(SceneA, to = SceneB) {}
         }
 
-        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
-
-        val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.5f))
+        val dragController =
+            onDragStarted(
+                position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f),
+                overSlop = up(0.5f),
+            )
         val transition = assertThat(transitionState).isSceneTransition()
         assertThat(transition).hasFromScene(SceneA)
         assertThat(transition).hasToScene(SceneB)
@@ -739,13 +668,15 @@
     fun overscroll_releaseBetween0And100Percent_down() = runGestureTest {
         // Make scene C overscrollable.
         layoutState.transitions = transitions {
-            defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
+            defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
             from(SceneA, to = SceneC) {}
         }
 
-        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
-
-        val dragController = onDragStarted(pointersInfo = middle, overSlop = down(0.5f))
+        val dragController =
+            onDragStarted(
+                position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f),
+                overSlop = down(0.5f),
+            )
         val transition = assertThat(transitionState).isSceneTransition()
         assertThat(transition).hasFromScene(SceneA)
         assertThat(transition).hasToScene(SceneC)
@@ -771,11 +702,9 @@
             from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
         }
 
-        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
-
         val dragController =
             onDragStarted(
-                pointersInfo = middle,
+                position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f),
                 overSlop = up(1.5f),
                 expectedConsumedOverSlop = up(1f),
             )
@@ -805,11 +734,9 @@
             from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
         }
 
-        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
-
         val dragController =
             onDragStarted(
-                pointersInfo = middle,
+                position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f),
                 overSlop = down(1.5f),
                 expectedConsumedOverSlop = down(1f),
             )
@@ -944,33 +871,4 @@
         assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
         assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
     }
-
-    @Test
-    fun replaceOverlayNestedScroll() = runGestureTest {
-        layoutState.showOverlay(OverlayA, animationScope = testScope)
-        advanceUntilIdle()
-
-        // Initial state.
-        assertThat(layoutState.transitionState).isIdle()
-        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
-        assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)
-
-        // Swipe down to replace overlay A by overlay B.
-
-        val nestedScroll = nestedScrollConnection()
-        nestedScroll.scroll(downOffset(0.1f))
-        val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition()
-        assertThat(transition).hasCurrentScene(SceneA)
-        assertThat(transition).hasFromOverlay(OverlayA)
-        assertThat(transition).hasToOverlay(OverlayB)
-        assertThat(transition).hasCurrentOverlays(OverlayA)
-        assertThat(transition).hasProgress(0.1f)
-
-        nestedScroll.preFling(Velocity(0f, velocityThreshold))
-        advanceUntilIdle()
-        // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
-        assertThat(layoutState.transitionState).isIdle()
-        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
-        assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
-    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 53495be..0051469 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -618,7 +618,7 @@
     fun layoutGetsCurrentTransitionStateFromComposition() {
         val state =
             rule.runOnUiThread {
-                MutableSceneTransitionLayoutStateImpl(
+                MutableSceneTransitionLayoutState(
                     SceneA,
                     transitions {
                         from(SceneA, to = SceneB) {
@@ -1126,7 +1126,7 @@
 
         val state =
             rule.runOnUiThread {
-                MutableSceneTransitionLayoutStateImpl(
+                MutableSceneTransitionLayoutState(
                     SceneA,
                     transitions {
                         from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) }
@@ -1331,7 +1331,7 @@
         val fooSize = 100.dp
         val state =
             rule.runOnUiThread {
-                MutableSceneTransitionLayoutStateImpl(
+                MutableSceneTransitionLayoutState(
                     SceneA,
                     transitions {
                         from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) }
@@ -1439,7 +1439,7 @@
     @Test
     fun targetStateIsSetEvenWhenNotPlaced() {
         // Start directly at A => B but with progress < 0f to overscroll on A.
-        val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl(SceneA) }
+        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
 
         lateinit var layoutImpl: SceneTransitionLayoutImpl
         val scope =
@@ -1473,7 +1473,7 @@
     fun lastAlphaIsNotSetByOutdatedLayer() {
         val state =
             rule.runOnUiThread {
-                MutableSceneTransitionLayoutStateImpl(
+                MutableSceneTransitionLayoutState(
                     SceneA,
                     transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } },
                 )
@@ -1537,7 +1537,7 @@
     fun fadingElementsDontAppearInstantly() {
         val state =
             rule.runOnUiThread {
-                MutableSceneTransitionLayoutStateImpl(
+                MutableSceneTransitionLayoutState(
                     SceneA,
                     transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } },
                 )
@@ -1583,7 +1583,7 @@
 
     @Test
     fun lastPlacementValuesAreClearedOnNestedElements() {
-        val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+        val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) }
 
         @Composable
         fun ContentScope.NestedFooBar() {
@@ -1658,7 +1658,7 @@
     fun currentTransitionSceneIsUsedToComputeElementValues() {
         val state =
             rule.runOnIdle {
-                MutableSceneTransitionLayoutStateImpl(
+                MutableSceneTransitionLayoutState(
                     SceneA,
                     transitions {
                         from(SceneB, to = SceneC) {
@@ -1709,7 +1709,7 @@
 
     @Test
     fun interruptionDeltasAreProperlyCleaned() {
-        val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+        val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) }
 
         @Composable
         fun ContentScope.Foo(offset: Dp) {
@@ -1780,7 +1780,7 @@
     fun transparentElementIsNotImpactingInterruption() {
         val state =
             rule.runOnIdle {
-                MutableSceneTransitionLayoutStateImpl(
+                MutableSceneTransitionLayoutState(
                     SceneA,
                     transitions {
                         from(SceneA, to = SceneB) {
@@ -1856,7 +1856,7 @@
 
     @Test
     fun replacedTransitionDoesNotTriggerInterruption() {
-        val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+        val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) }
 
         @Composable
         fun ContentScope.Foo(modifier: Modifier = Modifier) {
@@ -2027,7 +2027,7 @@
     ): SceneTransitionLayoutImpl {
         val state =
             rule.runOnIdle {
-                MutableSceneTransitionLayoutStateImpl(
+                MutableSceneTransitionLayoutState(
                     from,
                     transitions { from(from, to = to, preview = preview, builder = transition) },
                 )
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
deleted file mode 100644
index 5c6f91b..0000000
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
+++ /dev/null
@@ -1,866 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.animation.scene
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.rememberScrollableState
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.test.TouchInjectionScope
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Velocity
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.compose.modifiers.thenIf
-import com.google.common.truth.Truth.assertThat
-import kotlin.properties.Delegates
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.isActive
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class MultiPointerDraggableTest {
-    @get:Rule val rule = createComposeRule()
-
-    private val emptyConnection = object : NestedScrollConnection {}
-    private val defaultDispatcher = NestedScrollDispatcher()
-
-    private fun Modifier.nestedScrollDispatcher() = nestedScroll(emptyConnection, defaultDispatcher)
-
-    private class SimpleDragController(
-        val onDrag: (delta: Float) -> Unit,
-        val onStop: (velocity: Float) -> Unit,
-    ) : DragController {
-        override fun onDrag(delta: Float): Float {
-            onDrag.invoke(delta)
-            return delta
-        }
-
-        override suspend fun onStop(velocity: Float): Float {
-            onStop.invoke(velocity)
-            return velocity
-        }
-
-        override fun onCancel() {
-            error("MultiPointerDraggable never calls onCancel()")
-        }
-    }
-
-    @Test
-    fun cancellingPointerCallsOnDragStopped() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-
-        var enabled by mutableStateOf(false)
-        var started = false
-        var dragged = false
-        var stopped = false
-
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
-                    .nestedScrollDispatcher()
-                    .thenIf(enabled) {
-                        Modifier.multiPointerDraggable(
-                            orientation = Orientation.Vertical,
-                            onDragStarted = { _, _ ->
-                                started = true
-                                SimpleDragController(
-                                    onDrag = { dragged = true },
-                                    onStop = { stopped = true },
-                                )
-                            },
-                            dispatcher = defaultDispatcher,
-                        )
-                    }
-            )
-        }
-
-        fun startDraggingDown() {
-            rule.onRoot().performTouchInput {
-                down(middle)
-                moveBy(Offset(0f, touchSlop))
-            }
-        }
-
-        fun releaseFinger() {
-            rule.onRoot().performTouchInput { up() }
-        }
-
-        // Swiping down does nothing because enabled is false.
-        startDraggingDown()
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-        releaseFinger()
-
-        // Enable the draggable and swipe down. This should both call onDragStarted() and
-        // onDragDelta().
-        enabled = true
-        rule.waitForIdle()
-        startDraggingDown()
-        assertThat(started).isTrue()
-        assertThat(dragged).isTrue()
-        assertThat(stopped).isFalse()
-
-        // Disable the pointer input. This should call onDragStopped() even if didn't release the
-        // finger yet.
-        enabled = false
-        rule.waitForIdle()
-        assertThat(started).isTrue()
-        assertThat(dragged).isTrue()
-        assertThat(stopped).isTrue()
-    }
-
-    @Test
-    fun shouldNotStartDragEventsWith0PointersDown() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-
-        var started = false
-        var dragged = false
-        var stopped = false
-        var consumedByDescendant = false
-
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
-                    .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        onDragStarted = { _, _ ->
-                            started = true
-                            SimpleDragController(
-                                onDrag = { dragged = true },
-                                onStop = { stopped = true },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-                    .pointerInput(Unit) {
-                        coroutineScope {
-                            awaitPointerEventScope {
-                                while (isActive) {
-                                    val change = awaitPointerEvent().changes.first()
-                                    if (consumedByDescendant) {
-                                        change.consume()
-                                    }
-                                }
-                            }
-                        }
-                    }
-            )
-        }
-
-        // The first part of the gesture is consumed by our descendant
-        consumedByDescendant = true
-        rule.onRoot().performTouchInput {
-            down(middle)
-            moveBy(Offset(0f, touchSlop))
-        }
-
-        // The events were consumed by our descendant, we should not start a drag gesture.
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-
-        // The next events could be consumed by us
-        consumedByDescendant = false
-        rule.onRoot().performTouchInput {
-            // The pointer is moved to a new position without reporting it
-            updatePointerBy(0, Offset(0f, touchSlop))
-
-            // The pointer report an "up" (0 pointers down) with a new position
-            up()
-        }
-
-        // The "up" event should not be used to start a drag gesture
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-    }
-
-    @Test
-    fun handleDisappearingScrollableDuringAGesture() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-
-        var started = false
-        var dragged = false
-        var stopped = false
-        var consumedByScroll = false
-        var hasScrollable by mutableStateOf(true)
-
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
-                    .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        onDragStarted = { _, _ ->
-                            started = true
-                            SimpleDragController(
-                                onDrag = { dragged = true },
-                                onStop = { stopped = true },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-            ) {
-                if (hasScrollable) {
-                    Box(
-                        Modifier.scrollable(
-                                // Consume all the vertical scroll gestures
-                                rememberScrollableState(
-                                    consumeScrollDelta = {
-                                        consumedByScroll = true
-                                        it
-                                    }
-                                ),
-                                Orientation.Vertical,
-                            )
-                            .fillMaxSize()
-                    )
-                }
-            }
-        }
-
-        fun startDraggingDown() {
-            rule.onRoot().performTouchInput {
-                down(middle)
-                moveBy(Offset(0f, touchSlop))
-            }
-        }
-
-        fun continueDraggingDown() {
-            rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
-        }
-
-        fun releaseFinger() {
-            rule.onRoot().performTouchInput { up() }
-        }
-
-        // Swipe down. This should intercepted by the scrollable modifier.
-        startDraggingDown()
-        assertThat(consumedByScroll).isTrue()
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-
-        // Reset the scroll state for the test
-        consumedByScroll = false
-
-        // Suddenly remove the scrollable container
-        hasScrollable = false
-        rule.waitForIdle()
-
-        // Swipe down. This will be intercepted by multiPointerDraggable, it will wait touchSlop
-        // before consuming it.
-        continueDraggingDown()
-        assertThat(consumedByScroll).isFalse()
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-
-        // Swipe down. This should both call onDragStarted() and onDragDelta().
-        continueDraggingDown()
-        assertThat(consumedByScroll).isFalse()
-        assertThat(started).isTrue()
-        assertThat(dragged).isTrue()
-        assertThat(stopped).isFalse()
-
-        rule.waitForIdle()
-        releaseFinger()
-        assertThat(stopped).isTrue()
-    }
-
-    @Test
-    fun multiPointerWaitAConsumableEventInMainPass() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-
-        var started = false
-        var dragged = false
-        var stopped = false
-
-        var childConsumesOnPass: PointerEventPass? = null
-
-        suspend fun AwaitPointerEventScope.childPointerInputScope() {
-            awaitPointerEvent(PointerEventPass.Initial).also { initial ->
-                // Check unconsumed: it should be always true
-                assertThat(initial.changes.any { it.isConsumed }).isFalse()
-
-                if (childConsumesOnPass == PointerEventPass.Initial) {
-                    initial.changes.first().consume()
-                }
-            }
-
-            awaitPointerEvent(PointerEventPass.Main).also { main ->
-                // Check unconsumed
-                if (childConsumesOnPass != PointerEventPass.Initial) {
-                    assertThat(main.changes.any { it.isConsumed }).isFalse()
-                }
-
-                if (childConsumesOnPass == PointerEventPass.Main) {
-                    main.changes.first().consume()
-                }
-            }
-        }
-
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
-                    .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        onDragStarted = { _, _ ->
-                            started = true
-                            SimpleDragController(
-                                onDrag = { dragged = true },
-                                onStop = { stopped = true },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-            ) {
-                Box(
-                    Modifier.pointerInput(Unit) {
-                            coroutineScope {
-                                awaitPointerEventScope {
-                                    while (isActive) {
-                                        childPointerInputScope()
-                                    }
-                                }
-                            }
-                        }
-                        .fillMaxSize()
-                )
-            }
-        }
-
-        fun startDraggingDown() {
-            rule.onRoot().performTouchInput {
-                down(middle)
-                moveBy(Offset(0f, touchSlop))
-            }
-        }
-
-        fun continueDraggingDown() {
-            rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
-        }
-
-        childConsumesOnPass = PointerEventPass.Initial
-
-        startDraggingDown()
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-
-        continueDraggingDown()
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-
-        childConsumesOnPass = PointerEventPass.Main
-
-        continueDraggingDown()
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-
-        continueDraggingDown()
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-
-        childConsumesOnPass = null
-
-        // Swipe down. This will be intercepted by multiPointerDraggable, it will wait touchSlop
-        // before consuming it.
-        continueDraggingDown()
-        assertThat(started).isFalse()
-        assertThat(dragged).isFalse()
-        assertThat(stopped).isFalse()
-
-        // Swipe down. This should both call onDragStarted() and onDragDelta().
-        continueDraggingDown()
-        assertThat(started).isTrue()
-        assertThat(dragged).isTrue()
-        assertThat(stopped).isFalse()
-
-        childConsumesOnPass = PointerEventPass.Main
-
-        continueDraggingDown()
-        assertThat(stopped).isTrue()
-
-        // Complete the gesture
-        rule.onRoot().performTouchInput { up() }
-    }
-
-    @Test
-    fun multiPointerDuringAnotherGestureWaitAConsumableEventAfterMainPass() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-
-        var verticalStarted = false
-        var verticalDragged = false
-        var verticalStopped = false
-        var horizontalStarted = false
-        var horizontalDragged = false
-        var horizontalStopped = false
-
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
-                    .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        onDragStarted = { _, _ ->
-                            verticalStarted = true
-                            SimpleDragController(
-                                onDrag = { verticalDragged = true },
-                                onStop = { verticalStopped = true },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-                    .multiPointerDraggable(
-                        orientation = Orientation.Horizontal,
-                        onDragStarted = { _, _ ->
-                            horizontalStarted = true
-                            SimpleDragController(
-                                onDrag = { horizontalDragged = true },
-                                onStop = { horizontalStopped = true },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-            )
-        }
-
-        fun startDraggingDown() {
-            rule.onRoot().performTouchInput {
-                down(middle)
-                moveBy(Offset(0f, touchSlop))
-            }
-        }
-
-        fun startDraggingRight() {
-            rule.onRoot().performTouchInput {
-                down(middle)
-                moveBy(Offset(touchSlop, 0f))
-            }
-        }
-
-        fun stopDragging() {
-            rule.onRoot().performTouchInput { up() }
-        }
-
-        fun continueDown() {
-            rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
-        }
-
-        fun continueRight() {
-            rule.onRoot().performTouchInput { moveBy(Offset(touchSlop, 0f)) }
-        }
-
-        startDraggingDown()
-        assertThat(verticalStarted).isTrue()
-        assertThat(verticalDragged).isTrue()
-        assertThat(verticalStopped).isFalse()
-
-        // Ignore right swipe, do not interrupt the dragging gesture.
-        continueRight()
-        assertThat(horizontalStarted).isFalse()
-        assertThat(horizontalDragged).isFalse()
-        assertThat(horizontalStopped).isFalse()
-        assertThat(verticalStopped).isFalse()
-
-        stopDragging()
-        assertThat(verticalStopped).isTrue()
-
-        verticalStarted = false
-        verticalDragged = false
-        verticalStopped = false
-
-        startDraggingRight()
-        assertThat(horizontalStarted).isTrue()
-        assertThat(horizontalDragged).isTrue()
-        assertThat(horizontalStopped).isFalse()
-
-        // Ignore down swipe, do not interrupt the dragging gesture.
-        continueDown()
-        assertThat(verticalStarted).isFalse()
-        assertThat(verticalDragged).isFalse()
-        assertThat(verticalStopped).isFalse()
-        assertThat(horizontalStopped).isFalse()
-
-        stopDragging()
-        assertThat(horizontalStopped).isTrue()
-    }
-
-    @Test
-    fun multiPointerSwipeDetectorInteraction() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-
-        var started = false
-
-        var capturedChange: PointerInputChange? = null
-        var swipeConsume = false
-
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
-                    .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        swipeDetector =
-                            object : SwipeDetector {
-                                override fun detectSwipe(change: PointerInputChange): Boolean {
-                                    capturedChange = change
-                                    return swipeConsume
-                                }
-                            },
-                        onDragStarted = { _, _ ->
-                            started = true
-                            SimpleDragController(
-                                onDrag = { /* do nothing */ },
-                                onStop = { /* do nothing */ },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-            ) {}
-        }
-
-        fun startDraggingDown() {
-            rule.onRoot().performTouchInput {
-                down(middle)
-                moveBy(Offset(0f, touchSlop))
-            }
-        }
-
-        fun dragDown() {
-            rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
-        }
-
-        startDraggingDown()
-        assertThat(capturedChange).isNotNull()
-        capturedChange = null
-        assertThat(started).isFalse()
-
-        swipeConsume = true
-        // Drag in same direction
-        dragDown()
-        assertThat(capturedChange).isNotNull()
-        capturedChange = null
-
-        dragDown()
-        assertThat(capturedChange).isNull()
-
-        assertThat(started).isTrue()
-    }
-
-    @Test
-    fun multiPointerSwipeDetectorInteractionZeroOffsetFromStartPosition() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-
-        var started = false
-
-        var capturedChange: PointerInputChange? = null
-        var swipeConsume = false
-
-        var touchSlop = 0f
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
-                    .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        swipeDetector =
-                            object : SwipeDetector {
-                                override fun detectSwipe(change: PointerInputChange): Boolean {
-                                    capturedChange = change
-                                    return swipeConsume
-                                }
-                            },
-                        onDragStarted = { _, _ ->
-                            started = true
-                            SimpleDragController(
-                                onDrag = { /* do nothing */ },
-                                onStop = { /* do nothing */ },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-            ) {}
-        }
-
-        fun startDraggingDown() {
-            rule.onRoot().performTouchInput {
-                down(middle)
-                moveBy(Offset(0f, touchSlop))
-            }
-        }
-
-        fun dragUp() {
-            rule.onRoot().performTouchInput { moveBy(Offset(0f, -touchSlop)) }
-        }
-
-        startDraggingDown()
-        assertThat(capturedChange).isNotNull()
-        capturedChange = null
-        assertThat(started).isFalse()
-
-        swipeConsume = true
-        // Drag in the opposite direction
-        dragUp()
-        assertThat(capturedChange).isNotNull()
-        capturedChange = null
-
-        dragUp()
-        assertThat(capturedChange).isNull()
-
-        assertThat(started).isTrue()
-    }
-
-    @Test
-    fun multiPointerNestedScrollDispatcher() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-        var touchSlop = 0f
-
-        var consumedOnPreScroll = 0f
-
-        var availableOnPreScroll = Float.MIN_VALUE
-        var availableOnPostScroll = Float.MIN_VALUE
-        var availableOnPreFling = Float.MIN_VALUE
-        var availableOnPostFling = Float.MIN_VALUE
-
-        var consumedOnDrag = 0f
-        var consumedOnDragStop = 0f
-
-        val connection =
-            object : NestedScrollConnection {
-                override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
-                    availableOnPreScroll = available.y
-                    return Offset(0f, consumedOnPreScroll)
-                }
-
-                override fun onPostScroll(
-                    consumed: Offset,
-                    available: Offset,
-                    source: NestedScrollSource,
-                ): Offset {
-                    availableOnPostScroll = available.y
-                    return Offset.Zero
-                }
-
-                override suspend fun onPreFling(available: Velocity): Velocity {
-                    availableOnPreFling = available.y
-                    return Velocity.Zero
-                }
-
-                override suspend fun onPostFling(
-                    consumed: Velocity,
-                    available: Velocity,
-                ): Velocity {
-                    availableOnPostFling = available.y
-                    return Velocity.Zero
-                }
-            }
-
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            Box(
-                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
-                    .nestedScroll(connection)
-                    .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        onDragStarted = { _, _ ->
-                            SimpleDragController(
-                                onDrag = { consumedOnDrag = it },
-                                onStop = { consumedOnDragStop = it },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-            )
-        }
-
-        fun startDrag() {
-            rule.onRoot().performTouchInput {
-                down(middle)
-                moveBy(Offset(0f, touchSlop))
-            }
-        }
-
-        fun continueDrag() {
-            rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
-        }
-
-        fun stopDrag() {
-            rule.onRoot().performTouchInput { up() }
-        }
-
-        startDrag()
-
-        continueDrag()
-        assertThat(availableOnPreScroll).isEqualTo(touchSlop)
-        assertThat(consumedOnDrag).isEqualTo(touchSlop)
-        assertThat(availableOnPostScroll).isEqualTo(0f)
-
-        // Parent node consumes half of the gesture
-        consumedOnPreScroll = touchSlop / 2f
-        continueDrag()
-        assertThat(availableOnPreScroll).isEqualTo(touchSlop)
-        assertThat(consumedOnDrag).isEqualTo(touchSlop / 2f)
-        assertThat(availableOnPostScroll).isEqualTo(0f)
-
-        // Parent node consumes the gesture
-        consumedOnPreScroll = touchSlop
-        continueDrag()
-        assertThat(availableOnPreScroll).isEqualTo(touchSlop)
-        assertThat(consumedOnDrag).isEqualTo(0f)
-        assertThat(availableOnPostScroll).isEqualTo(0f)
-
-        // Parent node can intercept the velocity on stop
-        stopDrag()
-        assertThat(availableOnPreFling).isEqualTo(consumedOnDragStop)
-        assertThat(availableOnPostFling).isEqualTo(0f)
-    }
-
-    @Test
-    fun multiPointerOnStopVelocity() {
-        val size = 200f
-        val middle = Offset(size / 2f, size / 2f)
-
-        var stopped = false
-        var lastVelocity = -1f
-        var touchSlop = 0f
-        var density: Density by Delegates.notNull()
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            density = LocalDensity.current
-            Box(
-                Modifier.size(with(density) { Size(size, size).toDpSize() })
-                    .nestedScrollDispatcher()
-                    .multiPointerDraggable(
-                        orientation = Orientation.Vertical,
-                        onDragStarted = { _, _ ->
-                            SimpleDragController(
-                                onDrag = { /* do nothing */ },
-                                onStop = {
-                                    stopped = true
-                                    lastVelocity = it
-                                },
-                            )
-                        },
-                        dispatcher = defaultDispatcher,
-                    )
-            )
-        }
-
-        var eventMillis: Long by Delegates.notNull()
-        rule.onRoot().performTouchInput { eventMillis = eventPeriodMillis }
-
-        fun swipeGesture(block: TouchInjectionScope.() -> Unit) {
-            stopped = false
-            rule.onRoot().performTouchInput {
-                down(middle)
-                block()
-                up()
-            }
-            assertThat(stopped).isEqualTo(true)
-        }
-
-        val shortDistance = touchSlop / 2f
-        swipeGesture {
-            moveBy(delta = Offset(0f, shortDistance), delayMillis = eventMillis)
-            moveBy(delta = Offset(0f, shortDistance), delayMillis = eventMillis)
-        }
-        assertThat(lastVelocity).isGreaterThan(0f)
-        assertThat(lastVelocity).isWithin(1f).of((shortDistance / eventMillis) * 1000f)
-
-        val longDistance = touchSlop * 4f
-        swipeGesture {
-            moveBy(delta = Offset(0f, longDistance), delayMillis = eventMillis)
-            moveBy(delta = Offset(0f, longDistance), delayMillis = eventMillis)
-        }
-        assertThat(lastVelocity).isGreaterThan(0f)
-        assertThat(lastVelocity).isWithin(1f).of((longDistance / eventMillis) * 1000f)
-
-        rule.onRoot().performTouchInput {
-            down(pointerId = 0, position = middle)
-            down(pointerId = 1, position = middle)
-            moveBy(pointerId = 0, delta = Offset(0f, longDistance), delayMillis = eventMillis)
-            moveBy(pointerId = 0, delta = Offset(0f, longDistance), delayMillis = eventMillis)
-            // The velocity should be:
-            // (longDistance / eventMillis) pixels/ms
-
-            // 1 pointer left, the second one
-            up(pointerId = 0)
-
-            // After a few events the velocity should be:
-            // (shortDistance / eventMillis) pixels/ms
-            repeat(10) {
-                moveBy(pointerId = 1, delta = Offset(0f, shortDistance), delayMillis = eventMillis)
-            }
-            up(pointerId = 1)
-        }
-        assertThat(lastVelocity).isGreaterThan(0f)
-        assertThat(lastVelocity).isWithin(1f).of((shortDistance / eventMillis) * 1000f)
-    }
-}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index e80805a..e036084 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -40,6 +40,7 @@
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ScrollWheel
+import androidx.compose.ui.test.TouchInjectionScope
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -55,6 +56,8 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestOverlays.OverlayA
+import com.android.compose.animation.scene.TestOverlays.OverlayB
 import com.android.compose.animation.scene.TestScenes.SceneA
 import com.android.compose.animation.scene.TestScenes.SceneB
 import com.android.compose.animation.scene.TestScenes.SceneC
@@ -125,7 +128,7 @@
                         mapOf(
                             Swipe.Down to SceneA,
                             Swipe.Down(pointerCount = 2) to SceneB,
-                            Swipe.Down(pointersType = PointerType.Mouse) to SceneD,
+                            Swipe.Down(pointerType = PointerType.Mouse) to SceneD,
                             Swipe.Down(fromSource = Edge.Top) to SceneB,
                             Swipe.Right(fromSource = Edge.Left) to SceneB,
                         )
@@ -977,4 +980,164 @@
         rule.waitForIdle()
         assertThat(state.transitionState).isSceneTransition()
     }
+
+    @Test
+    fun nestedScroll_useFromSourceInfo() {
+        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state) {
+                scene(
+                    SceneA,
+                    userActions =
+                        mapOf(Swipe.Down to SceneB, Swipe.Down(fromSource = Edge.Top) to SceneC),
+                ) {
+                    // Use a fullscreen nested scrollable to use the nested scroll connection.
+                    Box(
+                        Modifier.fillMaxSize()
+                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+                    )
+                }
+                scene(SceneB) { Box(Modifier.fillMaxSize()) }
+                scene(SceneC) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        // Swiping down from the middle of the screen leads to B.
+        rule.onRoot().performTouchInput {
+            down(center)
+            moveBy(Offset(0f, touchSlop + 1f))
+        }
+
+        var transition = assertThat(state.transitionState).isSceneTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneB)
+
+        // Release finger and wait to settle back to A.
+        rule.onRoot().performTouchInput { up() }
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneA)
+
+        // Swiping down from the top of the screen leads to B.
+        rule.onRoot().performTouchInput {
+            down(center.copy(y = 0f))
+            moveBy(Offset(0f, touchSlop + 1f))
+        }
+
+        transition = assertThat(state.transitionState).isSceneTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneC)
+    }
+
+    @Test
+    fun nestedScroll_ignoreMouseWheel() {
+        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state) {
+                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
+                    // Use a fullscreen nested scrollable to use the nested scroll connection.
+                    Box(
+                        Modifier.fillMaxSize()
+                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+                    )
+                }
+                scene(SceneB) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        rule.onRoot().performMouseInput {
+            scroll(-touchSlop - 1f, scrollWheel = ScrollWheel.Vertical)
+        }
+        assertThat(state.transitionState).isIdle()
+    }
+
+    @Test
+    fun nestedScroll_keepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() {
+        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state) {
+                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
+                    // Use a fullscreen nested scrollable to use the nested scroll connection.
+                    Box(
+                        Modifier.fillMaxSize()
+                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+                    )
+                }
+                scene(SceneB) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        fun TouchInjectionScope.height() = bottom
+        fun TouchInjectionScope.halfHeight() = height() / 2f
+
+        rule.onRoot().performTouchInput {
+            down(center.copy(y = 0f))
+            moveBy(Offset(0f, touchSlop + halfHeight()))
+        }
+        val transition = assertThat(state.transitionState).isSceneTransition()
+        assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
+
+        // The progress should never go above 100%.
+        rule.onRoot().performTouchInput { moveBy(Offset(0f, height())) }
+        assertThat(transition).hasProgress(1f, tolerance = 0.01f)
+
+        // Because the overscroll effect of scene B is not attached, swiping in the opposite
+        // direction will directly decrease the progress.
+        rule.onRoot().performTouchInput { moveBy(Offset(0f, -halfHeight())) }
+        assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
+    }
+
+    @Test
+    fun nestedScroll_replaceOverlay() {
+        val state =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA))
+            }
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state) {
+                scene(SceneA) { Box(Modifier.fillMaxSize()) }
+                overlay(
+                    OverlayA,
+                    mapOf(Swipe.Down to UserActionResult.ReplaceByOverlay(OverlayB)),
+                ) {
+                    Box(
+                        Modifier.fillMaxSize()
+                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+                    )
+                }
+                overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        // Swipe down 100% to replace A by B.
+        rule.onRoot().performTouchInput {
+            down(center.copy(y = 0f))
+            moveBy(Offset(0f, touchSlop + bottom))
+        }
+
+        val transition = assertThat(state.transitionState).isReplaceOverlayTransition()
+        assertThat(transition).hasCurrentScene(SceneA)
+        assertThat(transition).hasFromOverlay(OverlayA)
+        assertThat(transition).hasToOverlay(OverlayB)
+        assertThat(transition).hasCurrentOverlays(OverlayA)
+        assertThat(transition).hasProgress(1f, tolerance = 0.01f)
+
+        // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
+        rule.onRoot().performTouchInput { up() }
+        assertThat(transition).hasCurrentScene(SceneA)
+        assertThat(transition).hasCurrentOverlays(OverlayB)
+
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneA)
+        assertThat(state.transitionState).hasCurrentOverlays(OverlayB)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
index cb87fe8..aada4a50 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -276,22 +276,22 @@
         val defaultSpec = spring<Float>(stiffness = 1f)
         val specFromAToC = spring<Float>(stiffness = 2f)
         val transitions = transitions {
-            defaultSwipeSpec = defaultSpec
+            defaultMotionSpatialSpec = defaultSpec
 
             from(SceneA, to = SceneB) {
                 // Default swipe spec.
             }
-            from(SceneA, to = SceneC) { swipeSpec = specFromAToC }
+            from(SceneA, to = SceneC) { motionSpatialSpec = specFromAToC }
         }
 
-        assertThat(transitions.defaultSwipeSpec).isSameInstanceAs(defaultSpec)
+        assertThat(transitions.defaultMotionSpatialSpec).isSameInstanceAs(defaultSpec)
 
         // A => B does not have a custom spec.
         assertThat(
                 transitions
                     .transitionSpec(from = SceneA, to = SceneB, key = null)
                     .transformationSpec(aToB())
-                    .swipeSpec
+                    .motionSpatialSpec
             )
             .isNull()
 
@@ -300,7 +300,7 @@
                 transitions
                     .transitionSpec(from = SceneA, to = SceneC, key = null)
                     .transformationSpec(transition(from = SceneA, to = SceneC))
-                    .swipeSpec
+                    .motionSpatialSpec
             )
             .isSameInstanceAs(specFromAToC)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt
index a8e390c..46d98f9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt
@@ -23,11 +23,16 @@
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runCurrent
 import com.android.systemui.kosmos.runTest
+import com.android.systemui.plugins.activityStarter
 import com.android.systemui.qs.panels.ui.viewmodel.toolbar.editModeButtonViewModelFactory
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -36,6 +41,15 @@
 
     val underTest = kosmos.editModeButtonViewModelFactory.create()
 
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            whenever(activityStarter.postQSRunnableDismissingKeyguard(any())).doAnswer {
+                (it.getArgument(0) as Runnable).run()
+            }
+        }
+    }
+
     @Test
     fun falsingFalseTap_editModeDoesntStart() =
         kosmos.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 75d000b..ac3089d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -283,14 +283,16 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            val intent = mock<PendingIntent>()
-            repo.setOngoingCallState(inCallModel(startTimeMs = 1000, intent = intent))
+            val pendingIntent = mock<PendingIntent>()
+            repo.setOngoingCallState(inCallModel(startTimeMs = 1000, intent = pendingIntent))
             val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
             assertThat(clickListener).isNotNull()
 
             clickListener!!.onClick(chipView)
 
-            verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
+            // Ensure that the SysUI didn't modify the notification's intent by verifying it
+            // directly matches the `PendingIntent` set -- see b/212467440.
+            verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(pendingIntent, null)
         }
 
     @Test
@@ -298,14 +300,16 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            val intent = mock<PendingIntent>()
-            repo.setOngoingCallState(inCallModel(startTimeMs = 0, intent = intent))
+            val pendingIntent = mock<PendingIntent>()
+            repo.setOngoingCallState(inCallModel(startTimeMs = 0, intent = pendingIntent))
             val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
             assertThat(clickListener).isNotNull()
 
             clickListener!!.onClick(chipView)
 
-            verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
+            // Ensure that the SysUI didn't modify the notification's intent by verifying it
+            // directly matches the `PendingIntent` set -- see b/212467440.
+            verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(pendingIntent, null)
         }
 
     companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
index c05b131..c5752691d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
@@ -35,6 +35,7 @@
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
+import android.platform.test.annotations.EnableFlags;
 
 import androidx.annotation.Nullable;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -61,6 +62,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
@@ -83,6 +85,7 @@
 
     private NotificationEntry mEntry;
     private NotifFilter mCapturedSuspendedFilter;
+    private NotifFilter mCapturedDndStatelessFilter;
     private NotifFilter mCapturedDozingFilter;
     private StatusBarStateController.StateListener mStatusBarStateCallback;
     private RankingCoordinator mRankingCoordinator;
@@ -107,6 +110,15 @@
         mRankingCoordinator.attach(mNotifPipeline);
         verify(mNotifPipeline, times(2)).addPreGroupFilter(mNotifFilterCaptor.capture());
         mCapturedSuspendedFilter = mNotifFilterCaptor.getAllValues().get(0);
+        if (com.android.systemui.Flags.notificationAmbientSuppressionAfterInflation()) {
+            verify(mNotifPipeline).addFinalizeFilter(mNotifFilterCaptor.capture());
+            mCapturedDndStatelessFilter = mNotifFilterCaptor.getAllValues().get(1);
+            mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(2);
+        } else {
+            verify(mNotifPipeline, never()).addFinalizeFilter(any());
+            mCapturedDndStatelessFilter = null;
+            mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(1);
+        }
         mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(1);
         mCapturedDozingFilter.setInvalidationListener(mInvalidationListener);
 
@@ -159,6 +171,40 @@
     }
 
     @Test
+    @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATION_AMBIENT_SUPPRESSION_AFTER_INFLATION)
+    public void filterStatelessVisualEffectsSuppression() {
+        Mockito.clearInvocations(mStatusBarStateController);
+
+        // WHEN should suppress ambient
+        mEntry.setRanking(getRankingForUnfilteredNotif()
+                .setSuppressedVisualEffects(SUPPRESSED_EFFECT_AMBIENT)
+                .build());
+
+        // THEN do not filter out the notification
+        assertFalse(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
+
+        // WHEN should suppress list
+        mEntry.setRanking(getRankingForUnfilteredNotif()
+                .setSuppressedVisualEffects(SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+                .build());
+
+        // THEN do not filter out the notification
+        assertFalse(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
+
+        // WHEN should suppress both ambient and list
+        mEntry.setRanking(getRankingForUnfilteredNotif()
+                .setSuppressedVisualEffects(
+                    SUPPRESSED_EFFECT_AMBIENT | SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+                .build());
+
+        // THEN we should filter out the notification!
+        assertTrue(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
+
+        // VERIFY that we don't check the dozing state
+        verify(mStatusBarStateController, never()).isDozing();
+    }
+
+    @Test
     public void filterDozingSuppressAmbient() {
         // GIVEN should suppress ambient
         mEntry.setRanking(getRankingForUnfilteredNotif()
@@ -200,7 +246,7 @@
 
         // WHEN it's not dozing (showing the notification list)
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        
+
         // THEN filter out the notification
         assertTrue(mCapturedDozingFilter.shouldFilterOut(mEntry, 0));
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index e1589b6..4e03933 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
@@ -45,13 +45,10 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.privacy.PrivacyItemController
 import com.android.systemui.privacy.logging.PrivacyLogger
-import com.android.systemui.screenrecord.RecordingController
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
 import com.android.systemui.statusbar.policy.BluetoothController
-import com.android.systemui.statusbar.policy.CastController
-import com.android.systemui.statusbar.policy.CastDevice
 import com.android.systemui.statusbar.policy.DataSaverController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.HotspotController
@@ -92,7 +89,6 @@
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
-import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.reset
 
@@ -108,8 +104,6 @@
     companion object {
         private const val ZEN_SLOT = "zen"
         private const val ALARM_SLOT = "alarm"
-        private const val CAST_SLOT = "cast"
-        private const val SCREEN_RECORD_SLOT = "screen_record"
         private const val CONNECTED_DISPLAY_SLOT = "connected_display"
         private const val MANAGED_PROFILE_SLOT = "managed_profile"
     }
@@ -117,7 +111,6 @@
     @Mock private lateinit var iconController: StatusBarIconController
     @Mock private lateinit var commandQueue: CommandQueue
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock private lateinit var castController: CastController
     @Mock private lateinit var hotspotController: HotspotController
     @Mock private lateinit var bluetoothController: BluetoothController
     @Mock private lateinit var nextAlarmController: NextAlarmController
@@ -133,7 +126,6 @@
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
     @Mock private lateinit var devicePolicyManagerResources: DevicePolicyResourcesManager
-    @Mock private lateinit var recordingController: RecordingController
     @Mock private lateinit var telecomManager: TelecomManager
     @Mock private lateinit var sharedPreferences: SharedPreferences
     @Mock private lateinit var dateFormatUtil: DateFormatUtil
@@ -302,101 +294,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun cast_chipsFlagOff_iconShown() {
-        statusBarPolicy.init()
-        clearInvocations(iconController)
-
-        val callbackCaptor = argumentCaptor<CastController.Callback>()
-        verify(castController).addCallback(callbackCaptor.capture())
-
-        whenever(castController.castDevices)
-            .thenReturn(
-                listOf(
-                    CastDevice(
-                        "id",
-                        "name",
-                        "description",
-                        CastDevice.CastState.Connected,
-                        CastDevice.CastOrigin.MediaProjection,
-                    )
-                )
-            )
-        callbackCaptor.firstValue.onCastDevicesChanged()
-
-        verify(iconController).setIconVisibility(CAST_SLOT, true)
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun cast_chipsFlagOn_noCallbackRegistered() {
-        statusBarPolicy.init()
-
-        verify(castController, never()).addCallback(any())
-    }
-
-    @Test
-    @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun screenRecord_chipsFlagOff_iconShown_forAllStates() {
-        statusBarPolicy.init()
-        clearInvocations(iconController)
-
-        val callbackCaptor = argumentCaptor<RecordingController.RecordingStateChangeCallback>()
-        verify(recordingController).addCallback(callbackCaptor.capture())
-
-        callbackCaptor.firstValue.onCountdown(3000)
-        testableLooper.processAllMessages()
-        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true)
-        clearInvocations(iconController)
-
-        callbackCaptor.firstValue.onCountdownEnd()
-        testableLooper.processAllMessages()
-        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false)
-        clearInvocations(iconController)
-
-        callbackCaptor.firstValue.onRecordingStart()
-        testableLooper.processAllMessages()
-        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true)
-        clearInvocations(iconController)
-
-        callbackCaptor.firstValue.onRecordingEnd()
-        testableLooper.processAllMessages()
-        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false)
-        clearInvocations(iconController)
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun screenRecord_chipsFlagOn_noCallbackRegistered() {
-        statusBarPolicy.init()
-
-        verify(recordingController, never()).addCallback(any())
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun screenRecord_chipsFlagOn_methodsDoNothing() {
-        statusBarPolicy.init()
-        clearInvocations(iconController)
-
-        statusBarPolicy.onCountdown(3000)
-        testableLooper.processAllMessages()
-        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
-
-        statusBarPolicy.onCountdownEnd()
-        testableLooper.processAllMessages()
-        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
-
-        statusBarPolicy.onRecordingStart()
-        testableLooper.processAllMessages()
-        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
-
-        statusBarPolicy.onRecordingEnd()
-        testableLooper.processAllMessages()
-        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
-    }
-
-    @Test
     @EnableFlags(android.app.Flags.FLAG_MODES_UI, android.app.Flags.FLAG_MODES_UI_ICONS)
     fun zenModeInteractorActiveModeChanged_showsModeIcon() =
         testScope.runTest {
@@ -514,7 +411,6 @@
             executor,
             testableLooper.looper,
             context.resources,
-            castController,
             hotspotController,
             bluetoothController,
             nextAlarmController,
@@ -530,7 +426,6 @@
             userManager,
             userTracker,
             devicePolicyManager,
-            recordingController,
             telecomManager,
             /* displayId = */ 0,
             sharedPreferences,
@@ -589,11 +484,6 @@
 
         override fun getConfig(): ZenModeConfig = throw NotImplementedError()
 
-        fun setConsolidatedPolicy(policy: NotificationManager.Policy) {
-            this.consolidatedPolicy = policy
-            callbacks.forEach { it.onConsolidatedPolicyChanged(consolidatedPolicy) }
-        }
-
         override fun getConsolidatedPolicy(): NotificationManager.Policy = consolidatedPolicy
 
         override fun getNextAlarm() = throw NotImplementedError()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index b984099..6da06a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -21,21 +21,18 @@
 import android.app.IUidObserver
 import android.app.PendingIntent
 import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper
 import android.view.LayoutInflater
 import android.view.View
 import android.widget.LinearLayout
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.plugins.activityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
@@ -50,7 +47,6 @@
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
-import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
@@ -77,7 +73,6 @@
 class OngoingCallControllerTest : SysuiTestCase() {
     private val kosmos = Kosmos()
 
-    private val clock = kosmos.fakeSystemClock
     private val mainExecutor = kosmos.fakeExecutor
     private val testScope = kosmos.testScope
     private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository
@@ -88,7 +83,6 @@
 
     private val mockSwipeStatusBarAwayGestureHandler = mock<SwipeStatusBarAwayGestureHandler>()
     private val mockOngoingCallListener = mock<OngoingCallListener>()
-    private val mockActivityStarter = kosmos.activityStarter
     private val mockIActivityManager = mock<IActivityManager>()
     private val mockStatusBarWindowController = mock<StatusBarWindowController>()
     private val mockStatusBarWindowControllerStore = mock<StatusBarWindowControllerStore>()
@@ -111,8 +105,6 @@
                 context,
                 ongoingCallRepository,
                 kosmos.activeNotificationsInteractor,
-                clock,
-                mockActivityStarter,
                 mainExecutor,
                 mockIActivityManager,
                 DumpManager(),
@@ -223,48 +215,6 @@
         )
     }
 
-    /** Regression test for b/192379214. */
-    @Test
-    @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun notifRepoHasCallNotif_notificationWhenIsZero_timeHidden() {
-        setNotifOnRepo(
-            activeNotificationModel(
-                key = "notif",
-                callType = CallType.Ongoing,
-                contentIntent = null,
-                whenTime = 0,
-            )
-        )
-
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-        )
-
-        assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
-            .isEqualTo(0)
-    }
-
-    @Test
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun notifRepoHasCallNotif_notificationWhenIsValid_timeShown() {
-        setNotifOnRepo(
-            activeNotificationModel(
-                key = "notif",
-                callType = CallType.Ongoing,
-                whenTime = clock.currentTimeMillis(),
-            )
-        )
-
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-        )
-
-        assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
-            .isGreaterThan(0)
-    }
-
     /** Regression test for b/194731244. */
     @Test
     fun repoHasCallNotif_updatedManyTimes_uidObserverOnlyRegisteredOnce() {
@@ -493,52 +443,6 @@
         verify(mockOngoingCallListener).onOngoingCallStateChanged(any())
     }
 
-    /** Regression test for b/212467440. */
-    @Test
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() {
-        val pendingIntent = mock<PendingIntent>()
-        setNotifOnRepo(
-            activeNotificationModel(
-                key = "notif",
-                uid = CALL_UID,
-                contentIntent = pendingIntent,
-                callType = CallType.Ongoing,
-            )
-        )
-
-        chipView.performClick()
-
-        // Ensure that the sysui didn't modify the notification's intent -- see b/212467440.
-        verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
-    }
-
-    @Test
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun callNotificationAdded_chipIsClickable() {
-        setCallNotifOnRepo()
-
-        assertThat(chipView.hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun callNotificationAdded_newChipsEnabled_chipNotClickable() {
-        setCallNotifOnRepo()
-
-        assertThat(chipView.hasOnClickListeners()).isFalse()
-    }
-
-    @Test
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun fullscreenIsTrue_chipStillClickable() {
-        setCallNotifOnRepo()
-        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
-        testScope.runCurrent()
-
-        assertThat(chipView.hasOnClickListeners()).isTrue()
-    }
-
     @Test
     fun callStartedInImmersiveMode_swipeGestureCallbackAdded() {
         statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt
new file mode 100644
index 0000000..fec186e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2025 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.volume.dialog.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VolumeDialogCallbacksInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos().apply { useUnconfinedTestDispatcher() }
+
+    private val underTest: VolumeDialogCallbacksInteractor by lazy {
+        kosmos.volumeDialogCallbacksInteractor
+    }
+
+    @Test
+    fun initialEvent_isSubscribedToEvents() =
+        kosmos.runTest {
+            val event by collectLastValue(underTest.event)
+            assertThat(event).isInstanceOf(VolumeDialogEventModel.SubscribedToEvents::class.java)
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt
index bfafdab..1001c24 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt
@@ -16,75 +16,115 @@
 
 package com.android.systemui.volume.dialog.sliders.domain.interactor
 
+import android.media.AudioManager
+import android.service.notification.ZenPolicy
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.modes.TestModeBuilder
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.plugins.VolumeDialogController
 import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.testKosmos
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
 import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class VolumeDialogSliderInteractorTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos()
+    private val kosmos =
+        testKosmos().apply {
+            useUnconfinedTestDispatcher()
+            zenModeRepository.addMode(
+                TestModeBuilder()
+                    .setName("Blocks media, Active")
+                    .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
+                    .setActive(true)
+                    .build()
+            )
+        }
 
-    private lateinit var underTest: VolumeDialogSliderInteractor
-
-    @Before
-    fun setUp() {
-        underTest = kosmos.volumeDialogSliderInteractor
+    private val underTest: VolumeDialogSliderInteractor by lazy {
+        kosmos.volumeDialogSliderInteractor
     }
 
     @Test
     fun settingStreamVolume_setsActiveStream() =
-        with(kosmos) {
-            testScope.runTest {
-                runCurrent()
-                // initialize the stream model
-                fakeVolumeDialogController.setStreamVolume(volumeDialogSliderType.audioStream, 0)
+        kosmos.runTest {
+            // initialize the stream model
+            fakeVolumeDialogController.setStreamVolume(volumeDialogSliderType.audioStream, 0)
 
-                val sliderModel by collectLastValue(underTest.slider)
-                underTest.setStreamVolume(1)
-                runCurrent()
+            val sliderModel by collectLastValue(underTest.slider)
+            underTest.setStreamVolume(1)
 
-                assertThat(sliderModel!!.isActive).isTrue()
-            }
+            assertThat(sliderModel!!.isActive).isTrue()
         }
 
     @Test
     fun streamVolumeIs_minMaxAreEnforced() =
-        with(kosmos) {
-            testScope.runTest {
-                runCurrent()
-                fakeVolumeDialogController.updateState {
-                    states.put(
-                        volumeDialogSliderType.audioStream,
-                        VolumeDialogController.StreamState().apply {
-                            levelMin = 0
-                            level = 2
-                            levelMax = 1
-                        },
-                    )
-                }
-
-                val sliderModel by collectLastValue(underTest.slider)
-                runCurrent()
-
-                assertThat(sliderModel!!.level).isEqualTo(1)
+        kosmos.runTest {
+            fakeVolumeDialogController.updateState {
+                states.put(
+                    volumeDialogSliderType.audioStream,
+                    VolumeDialogController.StreamState().apply {
+                        levelMin = 0
+                        level = 2
+                        levelMax = 1
+                    },
+                )
             }
+
+            val sliderModel by collectLastValue(underTest.slider)
+
+            assertThat(sliderModel!!.level).isEqualTo(1)
+        }
+
+    @Test
+    fun streamCantBeBlockedByZenMode_isDisabledByZenMode_false() =
+        kosmos.runTest {
+            volumeDialogSliderType = VolumeDialogSliderType.Stream(AudioManager.STREAM_VOICE_CALL)
+
+            val isDisabledByZenMode by collectLastValue(underTest.isDisabledByZenMode)
+
+            assertThat(isDisabledByZenMode).isFalse()
+        }
+
+    @Test
+    fun remoteMediaStream_zenModeRestrictive_IsNotDisabledByZenMode() =
+        kosmos.runTest {
+            volumeDialogSliderType = VolumeDialogSliderType.RemoteMediaStream(0)
+
+            val isDisabledByZenMode by collectLastValue(underTest.isDisabledByZenMode)
+
+            assertThat(isDisabledByZenMode).isFalse()
+        }
+
+    @Test
+    fun audioSharingStream_zenModeRestrictive_IsNotDisabledByZenMode() =
+        kosmos.runTest {
+            volumeDialogSliderType = VolumeDialogSliderType.AudioSharingStream(0)
+
+            val isDisabledByZenMode by collectLastValue(underTest.isDisabledByZenMode)
+
+            assertThat(isDisabledByZenMode).isFalse()
+        }
+
+    @Test
+    fun streamBlockedByZenMode_isDisabledByZenMode_true() =
+        kosmos.runTest {
+            volumeDialogSliderType = VolumeDialogSliderType.Stream(AudioManager.STREAM_MUSIC)
+
+            val isDisabledByZenMode by collectLastValue(underTest.isDisabledByZenMode)
+
+            assertThat(isDisabledByZenMode).isTrue()
         }
 }
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 58f2d3c..67f620f 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -19,6 +19,7 @@
     android:id="@+id/volume_dialog_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:alpha="0"
     android:clipChildren="false"
     app:layoutDescription="@xml/volume_dialog_scene">
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 80fb8b9..a8ee609 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1963,7 +1963,7 @@
     <!-- Text displayed indicating that the user is connected to a satellite signal. -->
     <string name="satellite_connected_carrier_text">Satellite SOS</string>
     <!-- Text displayed indicating that the user might be able to use satellite SOS. -->
-    <string name="satellite_emergency_only_carrier_text">Emergency calls or SOS</string>
+    <string name="satellite_emergency_only_carrier_text">Emergency calls or SOS only</string>
 
     <!-- Content description skeleton. Input strings should be carrier name and signal bar description [CHAR LIMIT=NONE]-->
     <string name="accessibility_phone_string_format"><xliff:g id="carrier_name" example="Carrier Name">%1$s</xliff:g>, <xliff:g id="signal_strength_description" example="two bars">%2$s</xliff:g>.</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 622b67f..e22736b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -297,10 +297,13 @@
         DisappearAnimationUtils disappearAnimationUtils = needsSlowUnlockTransition
                         ? mDisappearAnimationUtilsLocked
                         : mDisappearAnimationUtils;
+        android.util.Log.i("KeyguardPINView", "startDisappearAnimation: " + finishRunnable);
         disappearAnimationUtils.createAnimation(
                 this, 0, 200, mDisappearYTranslation, false,
                 mDisappearAnimationUtils.getInterpolator(), () -> {
                     if (finishRunnable != null) {
+                        android.util.Log.i("KeyguardPINView",
+                                "startDisappearAnimation, invoking run()");
                         finishRunnable.run();
                     }
                 },
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
index 434a9ce..d5c815d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
@@ -170,6 +170,8 @@
 
                     launch {
                         viewModel.startDisappearAnimation.collect {
+                            android.util.Log.i("KeyguardBouncerViewBinder",
+                                    "viewModel.startDisappearAnimation: $it")
                             securityContainerController.startDisappearAnimation(it)
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt
index f606218..59990ea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.panels.ui.viewmodel.toolbar
 
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
+import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
 import dagger.assisted.AssistedFactory
@@ -27,11 +28,12 @@
 constructor(
     private val editModeViewModel: EditModeViewModel,
     private val falsingInteractor: FalsingInteractor,
+    private val activityStarter: ActivityStarter,
 ) {
 
     fun onButtonClick() {
         if (!falsingInteractor.isFalseTap(FalsingManager.LOW_PENALTY)) {
-            editModeViewModel.startEditing()
+            activityStarter.postQSRunnableDismissingKeyguard { editModeViewModel.startEditing() }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index c4306d3..19bf4c0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -3959,7 +3959,7 @@
             }
 
             final boolean isTrackpadThreeFingerSwipe = isTrackpadThreeFingerSwipe(event);
-            if (com.android.systemui.Flags.disableShadeExpandsOnTrackpadTwoFingerSwipe()
+            if (com.android.systemui.Flags.disableShadeTrackpadTwoFingerSwipe()
                     && !isTrackpadThreeFingerSwipe && isTwoFingerSwipeTrackpadEvent(event)
                     && !isPanelExpanded()) {
                 if (isDown) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
index 1d37dcf..69b069d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
@@ -74,7 +74,12 @@
         mStatusBarStateController.addCallback(mStatusBarStateCallback);
 
         pipeline.addPreGroupFilter(mSuspendedFilter);
-        pipeline.addPreGroupFilter(mDndVisualEffectsFilter);
+        if (com.android.systemui.Flags.notificationAmbientSuppressionAfterInflation()) {
+            pipeline.addPreGroupFilter(mDndPreGroupFilter);
+            pipeline.addFinalizeFilter(mDndVisualEffectsFilter);
+        } else {
+            pipeline.addPreGroupFilter(mDndVisualEffectsFilter);
+        }
     }
 
     public NotifSectioner getAlertingSectioner() {
@@ -191,6 +196,16 @@
         }
     };
 
+    private final NotifFilter mDndPreGroupFilter = new NotifFilter("DndPreGroupFilter") {
+        @Override
+        public boolean shouldFilterOut(NotificationEntry entry, long now) {
+            // Entries with both flags set should be suppressed ASAP regardless of dozing state.
+            // As a result of being doze-independent, they can also be suppressed early in the
+            // pipeline.
+            return entry.shouldSuppressNotificationList() && entry.shouldSuppressAmbient();
+        }
+    };
+
     private final StatusBarStateController.StateListener mStatusBarStateCallback =
             new StatusBarStateController.StateListener() {
                 private boolean mPrevDozeAmountIsOne = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index a2b4e7b..880df79d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -40,12 +40,10 @@
 import android.telecom.TelecomManager;
 import android.text.format.DateFormat;
 import android.util.Log;
-import android.view.View;
 
 import androidx.lifecycle.Observer;
 
 import com.android.internal.statusbar.StatusBarIcon;
-import com.android.systemui.Flags;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -60,13 +58,10 @@
 import com.android.systemui.qs.tiles.RotationLockTile;
 import com.android.systemui.res.R;
 import com.android.systemui.screenrecord.RecordingController;
-import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
 import com.android.systemui.statusbar.policy.BluetoothController;
-import com.android.systemui.statusbar.policy.CastController;
-import com.android.systemui.statusbar.policy.CastDevice;
 import com.android.systemui.statusbar.policy.DataSaverController;
 import com.android.systemui.statusbar.policy.DataSaverController.Listener;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -139,7 +134,6 @@
     private final TelecomManager mTelecomManager;
 
     private final Handler mHandler;
-    private final CastController mCast;
     private final HotspotController mHotspot;
     private final NextAlarmController mNextAlarmController;
     private final AlarmManager mAlarmManager;
@@ -161,7 +155,6 @@
     private final Executor mMainExecutor;
     private final Executor mUiBgExecutor;
     private final SensorPrivacyController mSensorPrivacyController;
-    private final RecordingController mRecordingController;
     private final RingerModeTracker mRingerModeTracker;
     private final PrivacyLogger mPrivacyLogger;
     private final ZenModeInteractor mZenModeInteractor;
@@ -180,7 +173,7 @@
     public PhoneStatusBarPolicy(StatusBarIconController iconController,
             CommandQueue commandQueue, BroadcastDispatcher broadcastDispatcher,
             @Main Executor mainExecutor, @UiBackground Executor uiBgExecutor, @Main Looper looper,
-            @Main Resources resources, CastController castController,
+            @Main Resources resources,
             HotspotController hotspotController, BluetoothController bluetoothController,
             NextAlarmController nextAlarmController, UserInfoController userInfoController,
             RotationLockController rotationLockController, DataSaverController dataSaverController,
@@ -190,7 +183,7 @@
             LocationController locationController,
             SensorPrivacyController sensorPrivacyController, AlarmManager alarmManager,
             UserManager userManager, UserTracker userTracker,
-            DevicePolicyManager devicePolicyManager, RecordingController recordingController,
+            DevicePolicyManager devicePolicyManager,
             @Nullable TelecomManager telecomManager, @DisplayId int displayId,
             @Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
             RingerModeTracker ringerModeTracker,
@@ -206,7 +199,6 @@
         mBroadcastDispatcher = broadcastDispatcher;
         mHandler = new Handler(looper);
         mResources = resources;
-        mCast = castController;
         mHotspot = hotspotController;
         mBluetooth = bluetoothController;
         mNextAlarmController = nextAlarmController;
@@ -223,7 +215,6 @@
         mLocationController = locationController;
         mPrivacyItemController = privacyItemController;
         mSensorPrivacyController = sensorPrivacyController;
-        mRecordingController = recordingController;
         mMainExecutor = mainExecutor;
         mUiBgExecutor = uiBgExecutor;
         mTelecomManager = telecomManager;
@@ -368,11 +359,6 @@
                     this::onMainActiveModeChanged);
         }
         mZenController.addCallback(mZenControllerCallback);
-        if (!Flags.statusBarScreenSharingChips()) {
-            // If the flag is enabled, the cast icon is handled in the new screen sharing chips
-            // instead of here so we don't need to listen for events here.
-            mCast.addCallback(mCastCallback);
-        }
         mHotspot.addCallback(mHotspotCallback);
         mNextAlarmController.addCallback(mNextAlarmCallback);
         mDataSaver.addCallback(this);
@@ -380,11 +366,6 @@
         mPrivacyItemController.addCallback(this);
         mSensorPrivacyController.addCallback(mSensorPrivacyListener);
         mLocationController.addCallback(this);
-        if (!Flags.statusBarScreenSharingChips()) {
-            // If the flag is enabled, the screen record icon is handled in the new screen sharing
-            // chips instead of here so we don't need to listen for events here.
-            mRecordingController.addCallback(this);
-        }
         mJavaAdapter.alwaysCollectFlow(mConnectedDisplayInteractor.getConnectedDisplayState(),
                 this::onConnectedDisplayAvailabilityChanged);
 
@@ -583,33 +564,6 @@
         }
     }
 
-    private void updateCast() {
-        if (Flags.statusBarScreenSharingChips()) {
-            // The cast icon is handled in the new screen sharing chips instead of here.
-            return;
-        }
-
-        boolean isCasting = false;
-        for (CastDevice device : mCast.getCastDevices()) {
-            if (device.isCasting()) {
-                isCasting = true;
-                break;
-            }
-        }
-        if (DEBUG) Log.v(TAG, "updateCast: isCasting: " + isCasting);
-        mHandler.removeCallbacks(mRemoveCastIconRunnable);
-        if (isCasting && !mRecordingController.isRecording()) { // screen record has its own icon
-            mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast,
-                    mResources.getString(R.string.accessibility_casting));
-            mIconController.setIconVisibility(mSlotCast, true);
-        } else {
-            // don't turn off the screen-record icon for a few seconds, just to make sure the user
-            // has seen it
-            if (DEBUG) Log.v(TAG, "updateCast: hiding icon in 3 sec...");
-            mHandler.postDelayed(mRemoveCastIconRunnable, 3000);
-        }
-    }
-
     private void updateProfileIcon() {
         // getLastResumedActivityUserId needs to acquire the AM lock, which may be contended in
         // some cases. Since it doesn't really matter here whether it's updated in this frame
@@ -678,13 +632,6 @@
         }
     };
 
-    private final CastController.Callback mCastCallback = new CastController.Callback() {
-        @Override
-        public void onCastDevicesChanged() {
-            updateCast();
-        }
-    };
-
     private final NextAlarmController.NextAlarmChangeCallback mNextAlarmCallback =
             new NextAlarmController.NextAlarmChangeCallback() {
                 @Override
@@ -856,85 +803,6 @@
         }
     };
 
-    private Runnable mRemoveCastIconRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (Flags.statusBarScreenSharingChips()) {
-                // The cast icon is handled in the new screen sharing chips instead of here.
-                return;
-            }
-            if (DEBUG) Log.v(TAG, "updateCast: hiding icon NOW");
-            mIconController.setIconVisibility(mSlotCast, false);
-        }
-    };
-
-    // Screen Recording
-    @Override
-    public void onCountdown(long millisUntilFinished) {
-        if (Flags.statusBarScreenSharingChips()) {
-            // The screen record icon is handled in the new screen sharing chips instead of here.
-            return;
-        }
-        if (DEBUG) Log.d(TAG, "screenrecord: countdown " + millisUntilFinished);
-        int countdown =
-                (int) ScreenRecordModel.Starting.Companion.toCountdownSeconds(millisUntilFinished);
-        int resourceId = R.drawable.stat_sys_screen_record;
-        String description = Integer.toString(countdown);
-        switch (countdown) {
-            case 1:
-                resourceId = R.drawable.stat_sys_screen_record_1;
-                break;
-            case 2:
-                resourceId = R.drawable.stat_sys_screen_record_2;
-                break;
-            case 3:
-                resourceId = R.drawable.stat_sys_screen_record_3;
-                break;
-        }
-        mIconController.setIcon(mSlotScreenRecord, resourceId, description);
-        mIconController.setIconVisibility(mSlotScreenRecord, true);
-        // Set as assertive so talkback will announce the countdown
-        mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord,
-                View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE);
-    }
-
-    @Override
-    public void onCountdownEnd() {
-        if (Flags.statusBarScreenSharingChips()) {
-            // The screen record icon is handled in the new screen sharing chips instead of here.
-            return;
-        }
-        if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown");
-        mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
-        // Reset talkback priority
-        mHandler.post(() -> mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord,
-                View.ACCESSIBILITY_LIVE_REGION_NONE));
-    }
-
-    @Override
-    public void onRecordingStart() {
-        if (Flags.statusBarScreenSharingChips()) {
-            // The screen record icon is handled in the new screen sharing chips instead of here.
-            return;
-        }
-        if (DEBUG) Log.d(TAG, "screenrecord: showing icon");
-        mIconController.setIcon(mSlotScreenRecord,
-                R.drawable.stat_sys_screen_record,
-                mResources.getString(R.string.screenrecord_ongoing_screen_only));
-        mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, true));
-    }
-
-    @Override
-    public void onRecordingEnd() {
-        if (Flags.statusBarScreenSharingChips()) {
-            // The screen record icon is handled in the new screen sharing chips instead of here.
-            return;
-        }
-        // Ensure this is on the main thread
-        if (DEBUG) Log.d(TAG, "screenrecord: hiding icon");
-        mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
-    }
-
     private void onConnectedDisplayAvailabilityChanged(ConnectedDisplayInteractor.State state) {
         boolean visible = state != ConnectedDisplayInteractor.State.DISCONNECTED;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index e622d8f..1f1be26 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -42,7 +42,6 @@
 import com.android.app.animation.InterpolatorsAndroidX;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
-import com.android.systemui.Flags;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
@@ -698,20 +697,9 @@
 
         boolean showClock = externalModel.getShowClock() && !headsUpVisible;
 
-        boolean showPrimaryOngoingActivityChip;
-        if (Flags.statusBarScreenSharingChips()) {
-            // If this flag is on, the ongoing activity status comes from
-            // CollapsedStatusBarViewBinder, which updates the mHasPrimaryOngoingActivity variable.
-            showPrimaryOngoingActivityChip = mHasPrimaryOngoingActivity;
-        } else {
-            // If this flag is off, the only ongoing activity is the ongoing call, and we pull it
-            // from the controller directly.
-            showPrimaryOngoingActivityChip = mOngoingCallController.hasOngoingCall();
-        }
+        boolean showPrimaryOngoingActivityChip = mHasPrimaryOngoingActivity;
         boolean showSecondaryOngoingActivityChip =
-                Flags.statusBarScreenSharingChips()
-                        && StatusBarNotifChips.isEnabled()
-                        && mHasSecondaryOngoingActivity;
+                StatusBarNotifChips.isEnabled() && mHasSecondaryOngoingActivity;
 
         return new StatusBarVisibilityModel(
                 showClock,
@@ -869,10 +857,6 @@
 
     /**
      * Displays the primary ongoing activity chip.
-     *
-     * If Flags.statusBarScreenSharingChips is disabled, this chip will only ever contain the
-     * ongoing call information, If that flag is enabled, it will support different kinds of ongoing
-     * activities. See b/332662551.
      */
     private void showPrimaryOngoingActivityChip(boolean animate) {
         StatusBarRootModernization.assertInLegacyMode();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index f56c2d5..4eb69ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -24,18 +24,14 @@
 import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.CoreStartable
 import com.android.systemui.Dumpable
-import com.android.systemui.Flags
-import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
-import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
@@ -49,7 +45,6 @@
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import com.android.systemui.statusbar.policy.CallbackController
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
-import com.android.systemui.util.time.SystemClock
 import java.io.PrintWriter
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -69,8 +64,6 @@
     private val context: Context,
     private val ongoingCallRepository: OngoingCallRepository,
     private val activeNotificationsInteractor: ActiveNotificationsInteractor,
-    private val systemClock: SystemClock,
-    private val activityStarter: ActivityStarter,
     @Main private val mainExecutor: Executor,
     private val iActivityManager: IActivityManager,
     private val dumpManager: DumpManager,
@@ -109,7 +102,6 @@
         scope.launch {
             statusBarModeRepository.defaultDisplay.isInFullscreenMode.collect {
                 isFullscreen = it
-                updateChipClickListener()
                 updateGestureListening()
             }
         }
@@ -194,7 +186,7 @@
 
         if (notifModel == null) {
             logger.log(TAG, LogLevel.DEBUG, {}, { "NotifInteractorCallModel: null" })
-            removeChip()
+            removeChipInfo()
         } else if (notifModel.callType != CallType.Ongoing) {
             logger.log(
                 TAG,
@@ -202,7 +194,7 @@
                 { str1 = notifModel.callType.name },
                 { "Notification Interactor sent ActiveNotificationModel with callType=$str1" },
             )
-            removeChip()
+            removeChipInfo()
         } else {
             logger.log(
                 TAG,
@@ -243,29 +235,10 @@
         val timeView = currentChipView?.getTimeView()
 
         if (currentChipView != null && timeView != null) {
-            if (!Flags.statusBarScreenSharingChips()) {
-                // If the new status bar screen sharing chips are enabled, then the display logic
-                // for *all* status bar chips (both the call chip and the screen sharing chips) are
-                // handled by CollapsedStatusBarViewBinder, *not* this class. We need to disable
-                // this class from making any display changes because the new chips use the same
-                // view as the old call chip.
-                // TODO(b/332662551): We should move this whole controller class to recommended
-                // architecture so that we don't need to awkwardly disable only some parts of this
-                // class.
-                if (currentCallNotificationInfo.hasValidStartTime()) {
-                    timeView.setShouldHideText(false)
-                    timeView.base =
-                        currentCallNotificationInfo.callStartTime -
-                            systemClock.currentTimeMillis() + systemClock.elapsedRealtime()
-                    timeView.start()
-                } else {
-                    timeView.setShouldHideText(true)
-                    timeView.stop()
-                }
-                updateChipClickListener()
-            }
-
-            // But, this class still needs to do the non-display logic regardless of the flag.
+            // Current behavior: Displaying the call chip is handled by HomeStatusBarViewBinder, but
+            // this class is still responsible for the non-display logic.
+            // Future behavior: if StatusBarChipsModernization flag is enabled, this class is
+            // completely deprecated and does nothing.
             uidObserver.registerWithUid(currentCallNotificationInfo.uid)
             if (!currentCallNotificationInfo.statusBarSwipedAway) {
                 statusBarWindowControllerStore.defaultDisplay
@@ -286,33 +259,6 @@
         }
     }
 
-    private fun updateChipClickListener() {
-        StatusBarChipsModernization.assertInLegacyMode()
-
-        if (Flags.statusBarScreenSharingChips()) {
-            return
-        }
-
-        if (callNotificationInfo == null) {
-            return
-        }
-        val currentChipView = chipView
-        val backgroundView =
-            currentChipView?.findViewById<View>(R.id.ongoing_activity_chip_background)
-        val intent = callNotificationInfo?.intent
-        if (currentChipView != null && backgroundView != null && intent != null) {
-            currentChipView.setOnClickListener {
-                activityStarter.postStartActivityDismissingKeyguard(
-                    intent,
-                    ActivityTransitionAnimator.Controller.fromView(
-                        backgroundView,
-                        InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
-                    ),
-                )
-            }
-        }
-    }
-
     /** Returns true if the given [procState] represents a process that's visible to the user. */
     private fun isProcessVisibleToUser(procState: Int): Boolean {
         StatusBarChipsModernization.assertInLegacyMode()
@@ -336,13 +282,10 @@
         }
     }
 
-    private fun removeChip() {
+    private fun removeChipInfo() {
         StatusBarChipsModernization.assertInLegacyMode()
 
         callNotificationInfo = null
-        if (!Flags.statusBarScreenSharingChips()) {
-            tearDownChipView()
-        }
         statusBarWindowControllerStore.defaultDisplay.setOngoingProcessRequiresStatusBarVisible(
             false
         )
@@ -395,13 +338,7 @@
         val isOngoing: Boolean,
         /** True if the user has swiped away the status bar while in this phone call. */
         val statusBarSwipedAway: Boolean,
-    ) {
-        /**
-         * Returns true if the notification information has a valid call start time. See
-         * b/192379214.
-         */
-        fun hasValidStartTime(): Boolean = callStartTime > 0
-    }
+    )
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.println("Active call notification: $callNotificationInfo")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 589db16..b762751 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -58,6 +58,7 @@
 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import java.io.PrintWriter
 import java.lang.ref.WeakReference
+import java.util.concurrent.ConcurrentHashMap
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -108,9 +109,8 @@
 ) : MobileConnectionsRepository, Dumpable {
 
     // TODO(b/333912012): for now, we are never invalidating the cache. We can do better though
-    private var subIdRepositoryCache:
-        MutableMap<Int, WeakReference<FullMobileConnectionRepository>> =
-        mutableMapOf()
+    private var subIdRepositoryCache =
+        ConcurrentHashMap<Int, WeakReference<FullMobileConnectionRepository>>()
 
     private val defaultNetworkName =
         NetworkNameModel.Default(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index 2cd0117..67fdb3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import java.util.concurrent.ConcurrentHashMap
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
@@ -62,7 +63,7 @@
     @Background private val scope: CoroutineScope,
 ) {
     @VisibleForTesting
-    val reuseCache = mutableMapOf<Int, Pair<MobileIconViewModel, CoroutineScope>>()
+    val reuseCache = ConcurrentHashMap<Int, Pair<MobileIconViewModel, CoroutineScope>>()
 
     val subscriptionIdsFlow: StateFlow<List<Int>> =
         interactor.filteredSubscriptions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index 31d6d86d..8daa803 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -24,7 +24,6 @@
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
-import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
@@ -116,11 +115,7 @@
                     }
                 }
 
-                if (
-                    Flags.statusBarScreenSharingChips() &&
-                        !StatusBarNotifChips.isEnabled &&
-                        !StatusBarChipsModernization.isEnabled
-                ) {
+                if (!StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) {
                     val primaryChipViewBinding =
                         OngoingActivityChipBinder.createBinding(
                             view.requireViewById(R.id.ongoing_activity_chip_primary)
@@ -166,11 +161,7 @@
                     }
                 }
 
-                if (
-                    Flags.statusBarScreenSharingChips() &&
-                        StatusBarNotifChips.isEnabled &&
-                        !StatusBarChipsModernization.isEnabled
-                ) {
+                if (StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) {
                     // Create view bindings here so we don't keep re-fetching child views each time
                     // the chip model changes.
                     val primaryChipViewBinding =
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
index 3b0c8a6..b52c107 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.channels.ProducerScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -54,9 +55,10 @@
         callbackFlow {
                 val producer = VolumeDialogEventModelProducer(this)
                 volumeDialogController.addCallback(producer, bgHandler)
+                send(VolumeDialogEventModel.SubscribedToEvents)
                 awaitClose { volumeDialogController.removeCallback(producer) }
             }
-            .buffer(BUFFER_CAPACITY)
+            .buffer(capacity = BUFFER_CAPACITY, onBufferOverflow = BufferOverflow.DROP_OLDEST)
             .shareIn(replay = 0, scope = coroutineScope, started = SharingStarted.WhileSubscribed())
 
     private class VolumeDialogEventModelProducer(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
index 51e7924..26d2414 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
@@ -31,7 +31,6 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
 
 /**
  * Exposes [VolumeDialogController.getState] in the [volumeDialogState].
@@ -65,12 +64,14 @@
                     is VolumeDialogEventModel.ShowSafetyWarning -> {
                         setSafetyWarning(VolumeDialogSafetyWarningModel.Visible(event.flags))
                     }
+                    is VolumeDialogEventModel.SubscribedToEvents -> {
+                        volumeDialogController.getState()
+                    }
                     else -> {
                         // do nothing
                     }
                 }
             }
-            .onStart { volumeDialogController.getState() }
             .launchIn(coroutineScope)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
index 80e4238..9793d2b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
@@ -52,4 +52,11 @@
         VolumeDialogEventModel
 
     data object VolumeChangedFromKey : VolumeDialogEventModel
+
+    /**
+     * Signals that the
+     * [com.android.systemui.volume.dialog.domain.interactor.VolumeDialogCallbacksInteractor] is
+     * ready to process the events.
+     */
+    data object SubscribedToEvents : VolumeDialogEventModel
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
index 3988acb..b86252d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
@@ -47,7 +47,7 @@
 ) {
 
     val isDisabledByZenMode: Flow<Boolean> =
-        if (sliderType is VolumeDialogSliderType.Stream) {
+        if (zenModeInteractor.canBeBlockedByZenMode(sliderType)) {
             zenModeInteractor.activeModesBlockingStream(AudioStream(sliderType.audioStream)).map {
                 it.mainMode != null
             }
@@ -75,3 +75,8 @@
         }
     }
 }
+
+private fun ZenModeInteractor.canBeBlockedByZenMode(sliderType: VolumeDialogSliderType): Boolean {
+    return sliderType is VolumeDialogSliderType.Stream &&
+        canBeBlockedByZenMode(AudioStream(sliderType.audioStream))
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
index daf4c82..71fe22b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
@@ -26,6 +26,7 @@
 import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -49,10 +50,10 @@
         isRoutedToBluetooth: Boolean,
     ): Flow<Drawable> {
         return combine(
-            zenModeInteractor.activeModesBlockingStream(AudioStream(stream)),
+            zenModeInteractor.activeModesBlockingStream(stream),
             ringerModeForStream(stream),
         ) { activeModesBlockingStream, ringerMode ->
-            if (activeModesBlockingStream.mainMode?.icon != null) {
+            if (activeModesBlockingStream?.mainMode?.icon != null) {
                 return@combine activeModesBlockingStream.mainMode.icon.drawable
             } else {
                 context.getDrawable(
@@ -141,3 +142,16 @@
         }
     }
 }
+
+private fun ZenModeInteractor.activeModesBlockingStream(stream: Int): Flow<ActiveZenModes?> {
+    return if (AudioStream.supportedStreamTypes.contains(stream)) {
+        val audioStream = AudioStream(stream)
+        if (canBeBlockedByZenMode(audioStream)) {
+            activeModesBlockingStream(audioStream)
+        } else {
+            flowOf(null)
+        }
+    } else {
+        flowOf(null)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index 46d7d5f..428dc6e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -80,7 +80,6 @@
             MutableStateFlow(WindowInsets.Builder().build())
         // Root view of the Volume Dialog.
         val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root)
-        root.alpha = 0f
 
         animateVisibility(root, dialog, viewModel.dialogVisibilityModel)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 37eb148..fd751d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -466,6 +466,28 @@
         assertNull(runner.delegate)
     }
 
+    @Test
+    fun concurrentListenerModification_doesNotThrow() {
+        // Need a second listener to trigger the concurrent modification.
+        activityTransitionAnimator.addListener(object : ActivityTransitionAnimator.Listener {})
+        `when`(listener.onTransitionAnimationStart()).thenAnswer {
+            activityTransitionAnimator.removeListener(listener)
+            listener
+        }
+
+        val runner = activityTransitionAnimator.createEphemeralRunner(controller)
+        runner.onAnimationStart(
+            TRANSIT_NONE,
+            arrayOf(fakeWindow()),
+            emptyArray(),
+            emptyArray(),
+            iCallback,
+        )
+
+        waitForIdleSync()
+        verify(listener).onTransitionAnimationStart()
+    }
+
     private fun controllerFactory(
         cookie: ActivityTransitionAnimator.TransitionCookie =
             mock(ActivityTransitionAnimator.TransitionCookie::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 30ab416..f9df6c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -16,7 +16,6 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import static com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
 
@@ -496,160 +495,6 @@
     }
 
   @Test
-  @DisableFlags({
-    FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
-    StatusBarRootModernization.FLAG_NAME,
-    StatusBarChipsModernization.FLAG_NAME
-  })
-  public void disable_noOngoingCall_chipHidden() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(false);
-
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
-    }
-
-  @Test
-  @DisableFlags({
-    FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
-    StatusBarRootModernization.FLAG_NAME,
-    StatusBarChipsModernization.FLAG_NAME
-  })
-  public void disable_hasOngoingCall_chipDisplayedAndNotificationIconsHidden() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
-
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
-        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
-    }
-
-  @Test
-  @DisableFlags({
-    FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
-    StatusBarRootModernization.FLAG_NAME,
-    StatusBarChipsModernization.FLAG_NAME
-  })
-  public void disable_hasOngoingCallButNotificationIconsDisabled_chipHidden() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
-
-        fragment.disable(DEFAULT_DISPLAY,
-                StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
-
-        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
-    }
-
-  @Test
-  @DisableFlags({
-    FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
-    StatusBarRootModernization.FLAG_NAME,
-    StatusBarChipsModernization.FLAG_NAME
-  })
-  public void disable_hasOngoingCallButAlsoHun_chipHidden() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
-        when(mHeadsUpAppearanceController.shouldHeadsUpStatusBarBeVisible()).thenReturn(true);
-
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
-    }
-
-  @Test
-  @DisableFlags({
-    FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
-    StatusBarRootModernization.FLAG_NAME,
-    StatusBarChipsModernization.FLAG_NAME
-  })
-  public void disable_ongoingCallEnded_chipHidden() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-
-        // Ongoing call started
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
-
-        // Ongoing call ended
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(false);
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
-
-        // Ongoing call started
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
-    }
-
-  @Test
-  @DisableFlags({
-    FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
-    StatusBarRootModernization.FLAG_NAME,
-    StatusBarChipsModernization.FLAG_NAME
-  })
-  public void disable_hasOngoingCall_hidesNotifsWithoutAnimation() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-        // Enable animations for testing so that we can verify we still aren't animating
-        fragment.enableAnimationsForTesting();
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        // Ongoing call started
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, true);
-
-        // Notification area is hidden without delay
-        assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01);
-        assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility());
-    }
-
-  @Test
-  @DisableFlags({
-    FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
-    StatusBarRootModernization.FLAG_NAME,
-    StatusBarChipsModernization.FLAG_NAME
-  })
-  public void screenSharingChipsDisabled_ignoresNewCallback() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-
-        // WHEN there *is* an ongoing call via old callback
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, true);
-
-        // WHEN there's *no* ongoing activity via new callback
-        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasPrimaryOngoingActivity= */ false,
-                /* hasSecondaryOngoingActivity= */ false,
-                /* shouldAnimate= */ false);
-
-        // THEN the old callback value is used, so the view is shown
-        assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
-
-        // WHEN there's *no* ongoing call via old callback
-        when(mOngoingCallController.hasOngoingCall()).thenReturn(false);
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        // WHEN there *are* ongoing activities via new callback
-        mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasPrimaryOngoingActivity= */ true,
-                /* hasSecondaryOngoingActivity= */ true,
-                /* shouldAnimate= */ false);
-
-        // THEN the old callback value is used, so the views are hidden
-        assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility());
-        assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility());
-    }
-
-  @Test
-  @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
   public void noOngoingActivity_chipHidden() {
         resumeAndGetFragment();
@@ -667,7 +512,6 @@
     }
 
   @Test
-  @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
   public void hasPrimaryOngoingActivity_primaryChipDisplayedAndNotificationIconsHidden() {
         resumeAndGetFragment();
@@ -683,7 +527,6 @@
 
   @Test
   @EnableFlags({
-    FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
     StatusBarNotifChips.FLAG_NAME,
     StatusBarRootModernization.FLAG_NAME,
     StatusBarChipsModernization.FLAG_NAME
@@ -712,7 +555,6 @@
     }
 
   @Test
-  @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
   @DisableFlags({
     StatusBarNotifChips.FLAG_NAME,
     StatusBarRootModernization.FLAG_NAME,
@@ -730,7 +572,7 @@
     }
 
   @Test
-  @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
+  @EnableFlags({StatusBarNotifChips.FLAG_NAME})
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
   public void hasSecondaryOngoingActivity_flagOn_secondaryChipShownAndNotificationIconsHidden() {
         resumeAndGetFragment();
@@ -745,7 +587,6 @@
     }
 
   @Test
-  @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
   @DisableFlags({
     StatusBarNotifChips.FLAG_NAME,
     StatusBarRootModernization.FLAG_NAME,
@@ -766,7 +607,7 @@
     }
 
   @Test
-  @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
+  @EnableFlags({StatusBarNotifChips.FLAG_NAME})
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
   public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
@@ -784,7 +625,6 @@
     }
 
   @Test
-  @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
   @DisableFlags({
     StatusBarNotifChips.FLAG_NAME,
     StatusBarRootModernization.FLAG_NAME,
@@ -805,7 +645,7 @@
     }
 
   @Test
-  @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
+  @EnableFlags({StatusBarNotifChips.FLAG_NAME})
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
   public void hasOngoingActivitiesButAlsoHun_chipsHidden_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
@@ -823,7 +663,6 @@
     }
 
   @Test
-  @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
   @DisableFlags({
     StatusBarNotifChips.FLAG_NAME,
     StatusBarRootModernization.FLAG_NAME,
@@ -850,7 +689,7 @@
     }
 
   @Test
-  @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
+  @EnableFlags({StatusBarNotifChips.FLAG_NAME})
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
   public void primaryOngoingActivityEnded_chipHidden_notifsFlagOn() {
         resumeAndGetFragment();
@@ -873,7 +712,7 @@
     }
 
   @Test
-  @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
+  @EnableFlags({StatusBarNotifChips.FLAG_NAME})
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
   public void secondaryOngoingActivityEnded_chipHidden() {
         resumeAndGetFragment();
@@ -896,7 +735,6 @@
     }
 
   @Test
-  @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
   @DisableFlags({
     StatusBarNotifChips.FLAG_NAME,
     StatusBarRootModernization.FLAG_NAME,
@@ -919,7 +757,7 @@
     }
 
   @Test
-  @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
+  @EnableFlags({StatusBarNotifChips.FLAG_NAME})
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
   public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
@@ -938,13 +776,12 @@
     }
 
   @Test
-  @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
   @DisableFlags({
     StatusBarNotifChips.FLAG_NAME,
     StatusBarRootModernization.FLAG_NAME,
     StatusBarChipsModernization.FLAG_NAME
   })
-  public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOff() {
+  public void ignoresOngoingCallController_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         // WHEN there *is* an ongoing call via old callback
@@ -975,9 +812,9 @@
     }
 
   @Test
-  @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
+  @EnableFlags({StatusBarNotifChips.FLAG_NAME})
   @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME})
-  public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOn() {
+  public void ignoresOngoingCallController_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         // WHEN there *is* an ongoing call via old callback
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
index 43eb93e..9d73ae3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
@@ -197,7 +197,7 @@
     }
 }
 
-private inline fun CopyOnWriteArraySet<VolumeDialogController.Callbacks>.sendEvent(
+private inline fun Collection<VolumeDialogController.Callbacks>.sendEvent(
     event: (callback: VolumeDialogController.Callbacks) -> Unit
 ) {
     for (callback in this) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt
index 8ae1332..639bb691 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt
@@ -18,13 +18,18 @@
 
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.plugins.activityStarter
 import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
 
 val Kosmos.editModeButtonViewModelFactory by
     Kosmos.Fixture {
         object : EditModeButtonViewModel.Factory {
             override fun create(): EditModeButtonViewModel {
-                return EditModeButtonViewModel(editModeViewModel, falsingInteractor)
+                return EditModeButtonViewModel(
+                    editModeViewModel,
+                    falsingInteractor,
+                    activityStarter,
+                )
             }
         }
     }
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index cffdfbd..1dba629b 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -5666,6 +5666,13 @@
         }
 
         public boolean canAccessAppWidget(Widget widget, int uid, String packageName) {
+            if (isDifferentPackageFromHost(widget.host, packageName)
+                    && isDifferentPackageFromProvider(widget.provider, packageName)) {
+                // Apps providing AppWidget are only allowed to access widgets provided by the
+                // same package. Similarly, apps hosting AppWidget are only allowed to access
+                // widgets hosted by the same package.
+                return false;
+            }
             if (isHostInPackageForUid(widget.host, uid, packageName)) {
                 // Apps hosting the AppWidget have access to it.
                 return true;
@@ -5768,6 +5775,19 @@
                     && provider.id.componentName.getPackageName().equals(packageName);
         }
 
+        private boolean isDifferentPackageFromHost(
+                @Nullable final Host host, @Nullable final String packageName) {
+            return packageName == null || host == null || host.id == null
+                || !packageName.equals(host.id.packageName);
+        }
+
+        private boolean isDifferentPackageFromProvider(
+                @Nullable final Provider provider, @Nullable final String packageName) {
+            return packageName == null || provider == null || provider.id == null
+                    || provider.id.componentName == null
+                    || !packageName.equals(provider.id.componentName.getPackageName());
+        }
+
         private boolean isProfileEnabled(int profileId) {
             final long identity = Binder.clearCallingIdentity();
             try {
diff --git a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java
index 6ced096..d8fbde4 100644
--- a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java
+++ b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java
@@ -16,7 +16,7 @@
 
 package com.android.server.backup;
 
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 
 import android.annotation.Nullable;
@@ -302,7 +302,7 @@
                 // that the package being backed up doesn't get stuck in restricted mode until the
                 // backup time-out elapses.
                 for (int token : mOperationStorage.operationTokensForPackage(packageName)) {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG,
                                 mUserIdMsg + "agentDisconnected: will handleCancel(all) for token:"
                                         + Integer.toHexString(token));
diff --git a/services/backup/java/com/android/server/backup/BackupManagerConstants.java b/services/backup/java/com/android/server/backup/BackupManagerConstants.java
index b753d01..6aa5e2c 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerConstants.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerConstants.java
@@ -16,8 +16,6 @@
 
 package com.android.server.backup;
 
-import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
-
 import android.app.AlarmManager;
 import android.app.job.JobInfo;
 import android.content.ContentResolver;
@@ -168,85 +166,55 @@
     // group the calls of these methods in a block syncrhonized on
     // a reference of this object.
     public synchronized long getKeyValueBackupIntervalMilliseconds() {
-        if (DEBUG_SCHEDULING) {
-            Slog.v(
-                    TAG,
-                    "getKeyValueBackupIntervalMilliseconds(...) returns "
-                            + mKeyValueBackupIntervalMilliseconds);
-        }
+        Slog.d(TAG, "getKeyValueBackupIntervalMilliseconds(...) returns "
+                + mKeyValueBackupIntervalMilliseconds);
         return mKeyValueBackupIntervalMilliseconds;
     }
 
     public synchronized long getKeyValueBackupFuzzMilliseconds() {
-        if (DEBUG_SCHEDULING) {
-            Slog.v(
-                    TAG,
-                    "getKeyValueBackupFuzzMilliseconds(...) returns "
-                            + mKeyValueBackupFuzzMilliseconds);
-        }
+        Slog.d(TAG, "getKeyValueBackupFuzzMilliseconds(...) returns "
+                + mKeyValueBackupFuzzMilliseconds);
         return mKeyValueBackupFuzzMilliseconds;
     }
 
     public synchronized boolean getKeyValueBackupRequireCharging() {
-        if (DEBUG_SCHEDULING) {
-            Slog.v(
-                    TAG,
-                    "getKeyValueBackupRequireCharging(...) returns "
-                            + mKeyValueBackupRequireCharging);
-        }
+        Slog.d(TAG,
+                "getKeyValueBackupRequireCharging(...) returns " + mKeyValueBackupRequireCharging);
         return mKeyValueBackupRequireCharging;
     }
 
     public synchronized int getKeyValueBackupRequiredNetworkType() {
-        if (DEBUG_SCHEDULING) {
-            Slog.v(
-                    TAG,
-                    "getKeyValueBackupRequiredNetworkType(...) returns "
-                            + mKeyValueBackupRequiredNetworkType);
-        }
+        Slog.d(TAG, "getKeyValueBackupRequiredNetworkType(...) returns "
+                + mKeyValueBackupRequiredNetworkType);
         return mKeyValueBackupRequiredNetworkType;
     }
 
     public synchronized long getFullBackupIntervalMilliseconds() {
-        if (DEBUG_SCHEDULING) {
-            Slog.v(
-                    TAG,
-                    "getFullBackupIntervalMilliseconds(...) returns "
-                            + mFullBackupIntervalMilliseconds);
-        }
+        Slog.d(TAG, "getFullBackupIntervalMilliseconds(...) returns "
+                + mFullBackupIntervalMilliseconds);
         return mFullBackupIntervalMilliseconds;
     }
 
     public synchronized boolean getFullBackupRequireCharging() {
-        if (DEBUG_SCHEDULING) {
-            Slog.v(TAG, "getFullBackupRequireCharging(...) returns " + mFullBackupRequireCharging);
-        }
+        Slog.d(TAG, "getFullBackupRequireCharging(...) returns " + mFullBackupRequireCharging);
         return mFullBackupRequireCharging;
     }
 
     public synchronized int getFullBackupRequiredNetworkType() {
-        if (DEBUG_SCHEDULING) {
-            Slog.v(
-                    TAG,
-                    "getFullBackupRequiredNetworkType(...) returns "
-                            + mFullBackupRequiredNetworkType);
-        }
+        Slog.d(TAG,
+                "getFullBackupRequiredNetworkType(...) returns " + mFullBackupRequiredNetworkType);
         return mFullBackupRequiredNetworkType;
     }
 
     /** Returns an array of package names that should be notified whenever a backup finishes. */
     public synchronized String[] getBackupFinishedNotificationReceivers() {
-        if (DEBUG_SCHEDULING) {
-            Slog.v(
-                    TAG,
-                    "getBackupFinishedNotificationReceivers(...) returns "
-                            + TextUtils.join(", ", mBackupFinishedNotificationReceivers));
-        }
+        Slog.d(TAG, "getBackupFinishedNotificationReceivers(...) returns " + TextUtils.join(", ",
+                mBackupFinishedNotificationReceivers));
         return mBackupFinishedNotificationReceivers;
     }
 
     public synchronized long getWakelockTimeoutMillis() {
-        Slog.v(TAG, "wakelock timeout: " + mWakelockTimeoutMillis);
+        Slog.d(TAG, "wakelock timeout: " + mWakelockTimeoutMillis);
         return mWakelockTimeoutMillis;
     }
 }
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 8804faf..5edf08c 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -94,9 +94,7 @@
  */
 public class BackupManagerService extends IBackupManager.Stub implements BackupManagerInternal {
     public static final String TAG = "BackupManagerService";
-    public static final boolean DEBUG = true;
-    public static final boolean MORE_DEBUG = false;
-    public static final boolean DEBUG_SCHEDULING = true;
+    public static final boolean DEBUG = false;
 
     @VisibleForTesting
     static final String DUMP_RUNNING_USERS_MESSAGE = "Backup Manager is running for users:";
@@ -187,9 +185,7 @@
                 mUserRemovedReceiver, new IntentFilter(Intent.ACTION_USER_REMOVED));
         UserHandle mainUser = getUserManager().getMainUser();
         mDefaultBackupUserId = mainUser == null ? UserHandle.USER_SYSTEM : mainUser.getIdentifier();
-        if (DEBUG) {
-            Slog.d(TAG, "Default backup user id = " + mDefaultBackupUserId);
-        }
+        Slog.d(TAG, "Default backup user id = " + mDefaultBackupUserId);
     }
 
     @VisibleForTesting
diff --git a/services/backup/java/com/android/server/backup/BackupWakeLock.java b/services/backup/java/com/android/server/backup/BackupWakeLock.java
new file mode 100644
index 0000000..d839e7a
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/BackupWakeLock.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 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.backup;
+
+import static com.android.server.backup.BackupManagerService.TAG;
+
+import android.annotation.Nullable;
+import android.os.PowerManager;
+import android.os.WorkSource;
+import android.util.Slog;
+
+/**
+ * Wrapper over {@link PowerManager.WakeLock} to prevent double-free exceptions on release()
+ * after quit().
+ *
+ * <p>There should be a single instance of this class per {@link UserBackupManagerService}.
+ */
+public class BackupWakeLock {
+    private final PowerManager.WakeLock mPowerManagerWakeLock;
+    private boolean mHasQuit = false;
+    private final String mUserIdMessage;
+    private final BackupManagerConstants mBackupManagerConstants;
+
+    public BackupWakeLock(PowerManager.WakeLock powerManagerWakeLock, int userId,
+            BackupManagerConstants backupManagerConstants) {
+        mPowerManagerWakeLock = powerManagerWakeLock;
+        mUserIdMessage = "[UserID:" + userId + "] ";
+        mBackupManagerConstants = backupManagerConstants;
+    }
+
+    /** Acquires the {@link PowerManager.WakeLock} if hasn't been quit. */
+    public synchronized void acquire() {
+        if (mHasQuit) {
+            Slog.d(TAG, mUserIdMessage + "Ignore wakelock acquire after quit: "
+                    + mPowerManagerWakeLock.getTag());
+            return;
+        }
+        // Set a timeout for the wakelock. Otherwise if we fail internally and never call
+        // release(), the device might stay awake and drain battery indefinitely.
+        mPowerManagerWakeLock.acquire(mBackupManagerConstants.getWakelockTimeoutMillis());
+        Slog.d(TAG, mUserIdMessage + "Acquired wakelock:" + mPowerManagerWakeLock.getTag());
+    }
+
+    /** Releases the {@link PowerManager.WakeLock} if hasn't been quit. */
+    public synchronized void release() {
+        if (mHasQuit) {
+            Slog.d(TAG, mUserIdMessage + "Ignore wakelock release after quit: "
+                    + mPowerManagerWakeLock.getTag());
+            return;
+        }
+
+        if (!mPowerManagerWakeLock.isHeld()) {
+            Slog.w(TAG, mUserIdMessage + "Wakelock not held: " + mPowerManagerWakeLock.getTag());
+            return;
+        }
+
+        mPowerManagerWakeLock.release();
+        Slog.d(TAG, mUserIdMessage + "Released wakelock:" + mPowerManagerWakeLock.getTag());
+    }
+
+    /**
+     * Returns true if the {@link PowerManager.WakeLock} has been acquired but not yet released.
+     */
+    public synchronized boolean isHeld() {
+        return mPowerManagerWakeLock.isHeld();
+    }
+
+    /** Release the {@link PowerManager.WakeLock} till it isn't held. */
+    public synchronized void quit() {
+        while (mPowerManagerWakeLock.isHeld()) {
+            Slog.d(TAG, mUserIdMessage + "Releasing wakelock: " + mPowerManagerWakeLock.getTag());
+            mPowerManagerWakeLock.release();
+        }
+        mHasQuit = true;
+    }
+
+    /** Calls {@link PowerManager.WakeLock#setWorkSource} on the underlying wake lock. */
+    public void setWorkSource(@Nullable WorkSource workSource) {
+        mPowerManagerWakeLock.setWorkSource(workSource);
+    }
+}
diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
index b343ec8..9f4407d9 100644
--- a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
@@ -180,9 +180,7 @@
                 Slog.e(TAG, "Key-value backup failed on package " + packageName);
                 return false;
             }
-            if (DEBUG) {
-                Slog.i(TAG, "Key-value backup success for package " + packageName);
-            }
+            Slog.i(TAG, "Key-value backup success for package " + packageName);
             return true;
         } catch (RemoteException e) {
             Slog.e(TAG, "Error invoking agent for backup on " + packageName + ". " + e);
@@ -210,9 +208,7 @@
                 AppMetadataBackupWriter writer =
                         new AppMetadataBackupWriter(output, mPackageManager);
 
-                if (DEBUG) {
-                    Slog.d(TAG, "Writing manifest for " + mPackage.packageName);
-                }
+                Slog.d(TAG, "Writing manifest for " + mPackage.packageName);
 
                 writer.backupManifest(
                         mPackage,
@@ -223,9 +219,7 @@
                         /* withApk */ false);
                 mManifestFile.delete();
 
-                if (DEBUG) {
-                    Slog.d(TAG, "Writing key-value package payload" + mPackage.packageName);
-                }
+                Slog.d(TAG, "Writing key-value package payload" + mPackage.packageName);
                 FullBackup.backupToTar(mPackage.packageName, FullBackup.KEY_VALUE_DATA_TOKEN, null,
                         mDataDir.getAbsolutePath(),
                         mBackupDataName.getAbsolutePath(),
@@ -283,9 +277,7 @@
             if (!mBackupManagerService.waitUntilOperationComplete(token)) {
                 Slog.e(TAG, "Full backup failed on package " + mCurrentPackage.packageName);
             } else {
-                if (DEBUG) {
-                    Slog.d(TAG, "Full package backup success: " + mCurrentPackage.packageName);
-                }
+                Slog.d(TAG, "Full package backup success: " + mCurrentPackage.packageName);
             }
         } catch (IOException e) {
             Slog.e(TAG, "Error backing up " + mCurrentPackage.packageName + ": " + e);
diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbRestoreEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbRestoreEngine.java
index 3184bd8..b68a0e4 100644
--- a/services/backup/java/com/android/server/backup/KeyValueAdbRestoreEngine.java
+++ b/services/backup/java/com/android/server/backup/KeyValueAdbRestoreEngine.java
@@ -89,10 +89,8 @@
             ParcelFileDescriptor newState = ParcelFileDescriptor.open(newStateName,
                     MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE);
 
-            if (DEBUG) {
-                Slog.i(TAG, "Starting restore of package " + pkg + " for version code "
+            Slog.i(TAG, "Starting restore of package " + pkg + " for version code "
                         + info.version);
-            }
             agent.doRestore(backupData, info.version, newState, mToken,
                     mBackupManagerService.getBackupManagerBinder());
         } catch (IOException e) {
diff --git a/services/backup/java/com/android/server/backup/KeyValueBackupJob.java b/services/backup/java/com/android/server/backup/KeyValueBackupJob.java
index 9a788be..30fdb65 100644
--- a/services/backup/java/com/android/server/backup/KeyValueBackupJob.java
+++ b/services/backup/java/com/android/server/backup/KeyValueBackupJob.java
@@ -16,7 +16,6 @@
 
 package com.android.server.backup;
 
-import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 
 import android.app.AlarmManager;
@@ -92,9 +91,7 @@
             if (delay <= 0) {
                 delay = interval + new Random().nextInt((int) fuzz);
             }
-            if (DEBUG_SCHEDULING) {
-                Slog.v(TAG, "Scheduling k/v pass in " + (delay / 1000 / 60) + " minutes");
-            }
+            Slog.d(TAG, "Scheduling k/v pass in " + (delay / 1000 / 60) + " minutes");
 
             JobInfo.Builder builder = new JobInfo.Builder(getJobIdForUserId(userId),
                     sKeyValueJobService)
diff --git a/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java b/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java
index 52108bf1..e17063a 100644
--- a/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java
+++ b/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java
@@ -240,7 +240,7 @@
         try {
             if (!mExisting.contains(ANCESTRAL_RECORD_KEY)) {
                 // The old state does not store info on ancestral record
-                Slog.v(
+                Slog.d(
                         TAG,
                         "No ancestral record version in the old state. Storing "
                                 + "ancestral record version key");
@@ -249,7 +249,7 @@
                 upgradingAncestralRecordVersion = true;
             } else if (mStoredAncestralRecordVersion != ANCESTRAL_RECORD_VERSION) {
                 // The current ancestral record version has changed from the old state
-                Slog.v(
+                Slog.d(
                         TAG,
                         "Ancestral record version has changed from old state. Storing"
                                 + "ancestral record version key");
diff --git a/services/backup/java/com/android/server/backup/ProcessedPackagesJournal.java b/services/backup/java/com/android/server/backup/ProcessedPackagesJournal.java
index edc2379..3a6f228 100644
--- a/services/backup/java/com/android/server/backup/ProcessedPackagesJournal.java
+++ b/services/backup/java/com/android/server/backup/ProcessedPackagesJournal.java
@@ -46,7 +46,6 @@
 final class ProcessedPackagesJournal {
     private static final String TAG = "ProcessedPackagesJournal";
     private static final String JOURNAL_FILE_NAME = "processed";
-    private static final boolean DEBUG = BackupManagerService.DEBUG;
 
     // using HashSet instead of ArraySet since we expect 100-500 elements range
     @GuardedBy("mProcessedPackages")
@@ -136,9 +135,7 @@
                 new BufferedInputStream(new FileInputStream(journalFile)))) {
             while (true) {
                 String packageName = oldJournal.readUTF();
-                if (DEBUG) {
-                    Slog.v(TAG, "   + " + packageName);
-                }
+                Slog.d(TAG, "   + " + packageName);
                 mProcessedPackages.add(packageName);
             }
         } catch (EOFException e) {
diff --git a/services/backup/java/com/android/server/backup/TransportManager.java b/services/backup/java/com/android/server/backup/TransportManager.java
index a792db0..d33bfec 100644
--- a/services/backup/java/com/android/server/backup/TransportManager.java
+++ b/services/backup/java/com/android/server/backup/TransportManager.java
@@ -62,7 +62,7 @@
 /** Handles in-memory bookkeeping of all BackupTransport objects. */
 public class TransportManager {
     private static final String TAG = "BackupTransportManager";
-    private static final boolean MORE_DEBUG = false;
+    private static final boolean DEBUG = false;
 
     @VisibleForTesting
     public static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST";
@@ -155,7 +155,7 @@
                 enabled = mPackageManager.getApplicationEnabledSetting(packageName);
             } catch (IllegalArgumentException ex) {
                 // packageName doesn't exist: likely due to a race with it being uninstalled.
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
                             + " was changed, but no longer exists."));
                 }
@@ -163,7 +163,7 @@
             }
             switch (enabled) {
                 case COMPONENT_ENABLED_STATE_ENABLED: {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
                                 + " was enabled."));
                     }
@@ -174,7 +174,7 @@
                     // Package is set to its default enabled state (as specified in its manifest).
                     // Unless explicitly specified in manifest, the default enabled state
                     // is 'enabled'. Here, we assume that default state always means enabled.
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
                                 + " was put in default enabled state."));
                     }
@@ -182,7 +182,7 @@
                     return;
                 }
                 case COMPONENT_ENABLED_STATE_DISABLED: {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
                                 + " was disabled."));
                     }
@@ -190,7 +190,7 @@
                     return;
                 }
                 case COMPONENT_ENABLED_STATE_DISABLED_USER: {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName
                                 + " was disabled by user."));
                     }
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index ac1f50f..5af2346 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -19,8 +19,6 @@
 import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_BACKUP_IN_FOREGROUND;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.internal.BackupHandler.MSG_BACKUP_OPERATION_TIMEOUT;
 import static com.android.server.backup.internal.BackupHandler.MSG_FULL_CONFIRMATION_TIMEOUT;
@@ -88,7 +86,6 @@
 import android.os.SELinux;
 import android.os.SystemClock;
 import android.os.UserHandle;
-import android.os.WorkSource;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArraySet;
@@ -174,88 +171,6 @@
 
 /** System service that performs backup/restore operations. */
 public class UserBackupManagerService {
-    /**
-     * Wrapper over {@link PowerManager.WakeLock} to prevent double-free exceptions on release()
-     * after quit().
-     */
-    public static class BackupWakeLock {
-        private final PowerManager.WakeLock mPowerManagerWakeLock;
-        private boolean mHasQuit = false;
-        private final int mUserId;
-        private final BackupManagerConstants mBackupManagerConstants;
-
-        public BackupWakeLock(PowerManager.WakeLock powerManagerWakeLock, int userId,
-                BackupManagerConstants backupManagerConstants) {
-            mPowerManagerWakeLock = powerManagerWakeLock;
-            mUserId = userId;
-            mBackupManagerConstants = backupManagerConstants;
-        }
-
-        /** Acquires the {@link PowerManager.WakeLock} if hasn't been quit. */
-        public synchronized void acquire() {
-            if (mHasQuit) {
-                Slog.v(
-                        TAG,
-                        addUserIdToLogMessage(
-                                mUserId,
-                                "Ignore wakelock acquire after quit: "
-                                        + mPowerManagerWakeLock.getTag()));
-                return;
-            }
-            // Set a timeout for the wakelock. Otherwise if we fail internally and never call
-            // release(), the device might stay awake and drain battery indefinitely.
-            mPowerManagerWakeLock.acquire(mBackupManagerConstants.getWakelockTimeoutMillis());
-            Slog.v(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId, "Acquired wakelock:" + mPowerManagerWakeLock.getTag()));
-        }
-
-        /** Releases the {@link PowerManager.WakeLock} if hasn't been quit. */
-        public synchronized void release() {
-            if (mHasQuit) {
-                Slog.v(
-                        TAG,
-                        addUserIdToLogMessage(
-                                mUserId,
-                                "Ignore wakelock release after quit: "
-                                        + mPowerManagerWakeLock.getTag()));
-                return;
-            }
-
-            if (!mPowerManagerWakeLock.isHeld()) {
-                Slog.w(TAG, addUserIdToLogMessage(mUserId,
-                        "Wakelock not held: " + mPowerManagerWakeLock.getTag()));
-                return;
-            }
-
-            mPowerManagerWakeLock.release();
-            Slog.v(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId, "Released wakelock:" + mPowerManagerWakeLock.getTag()));
-        }
-
-        /**
-         * Returns true if the {@link PowerManager.WakeLock} has been acquired but not yet released.
-         */
-        public synchronized boolean isHeld() {
-            return mPowerManagerWakeLock.isHeld();
-        }
-
-        /** Release the {@link PowerManager.WakeLock} till it isn't held. */
-        public synchronized void quit() {
-            while (mPowerManagerWakeLock.isHeld()) {
-                Slog.v(
-                        TAG,
-                        addUserIdToLogMessage(
-                                mUserId, "Releasing wakelock: " + mPowerManagerWakeLock.getTag()));
-                mPowerManagerWakeLock.release();
-            }
-            mHasQuit = true;
-        }
-    }
-
     // Persistently track the need to do a full init.
     private static final String INIT_SENTINEL_FILE_NAME = "_need_init_";
 
@@ -436,11 +351,7 @@
             currentTransport = null;
         }
 
-        if (DEBUG) {
-            Slog.v(
-                    TAG,
-                    addUserIdToLogMessage(userId, "Starting with transport " + currentTransport));
-        }
+        Slog.d(TAG, addUserIdToLogMessage(userId, "Starting with transport " + currentTransport));
         TransportManager transportManager =
                 new TransportManager(userId, context, transportWhitelist, currentTransport);
 
@@ -450,11 +361,7 @@
         HandlerThread userBackupThread =
                 new HandlerThread("backup-" + userId, Process.THREAD_PRIORITY_BACKGROUND);
         userBackupThread.start();
-        if (DEBUG) {
-            Slog.d(
-                    TAG,
-                    addUserIdToLogMessage(userId, "Started thread " + userBackupThread.getName()));
-        }
+        Slog.d(TAG, addUserIdToLogMessage(userId, "Started thread " + userBackupThread.getName()));
 
         return createAndInitializeService(
                 userId,
@@ -491,7 +398,7 @@
         // if so delete expired events and do not print them to dumpsys
         BackupManagerMonitorDumpsysUtils backupManagerMonitorDumpsysUtils =
                 new BackupManagerMonitorDumpsysUtils();
-        if (backupManagerMonitorDumpsysUtils.deleteExpiredBMMEvents() && DEBUG){
+        if (backupManagerMonitorDumpsysUtils.deleteExpiredBMMEvents()) {
             Slog.d(TAG, "BMM Events recorded for dumpsys have expired");
         }
         return new UserBackupManagerService(
@@ -766,20 +673,10 @@
         mSetupComplete = setupComplete;
     }
 
-    public BackupWakeLock getWakelock() {
+    public BackupWakeLock getWakeLock() {
         return mWakelock;
     }
 
-    /**
-     * Sets the {@link WorkSource} of the {@link PowerManager.WakeLock} returned by {@link
-     * #getWakelock()}.
-     */
-    @VisibleForTesting
-    public void setWorkSource(@Nullable WorkSource workSource) {
-        // TODO: This is for testing, unfortunately WakeLock is final and WorkSource is not exposed
-        mWakelock.mPowerManagerWakeLock.setWorkSource(workSource);
-    }
-
     public Handler getBackupHandler() {
         return mBackupHandler;
     }
@@ -937,7 +834,7 @@
     }
 
     private void initPackageTracking() {
-        if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "` tracking"));
+        if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "` tracking"));
 
         // Remember our ancestral dataset
         mTokenFile = new File(mBaseStateDir, "ancestral");
@@ -959,7 +856,7 @@
             }
         } catch (FileNotFoundException fnf) {
             // Probably innocuous
-            Slog.v(TAG, addUserIdToLogMessage(mUserId, "No ancestral data"));
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "No ancestral data"));
         } catch (IOException e) {
             Slog.w(TAG, addUserIdToLogMessage(mUserId, "Unable to read token file"), e);
         }
@@ -1038,16 +935,12 @@
                                         pkg.applicationInfo)) {
                             schedule.add(new FullBackupEntry(pkgName, lastBackup));
                         } else {
-                            if (DEBUG) {
-                                Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName
+                            Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName
                                         + " no longer eligible for full backup"));
-                            }
                         }
                     } catch (NameNotFoundException e) {
-                        if (DEBUG) {
-                            Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName
+                        Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName
                                     + " not installed; dropping from full backup"));
-                        }
                     }
                 }
 
@@ -1058,7 +951,7 @@
                             && mScheduledBackupEligibility.appIsEligibleForBackup(
                                     app.applicationInfo)) {
                         if (!foundApps.contains(app.packageName)) {
-                            if (MORE_DEBUG) {
+                            if (DEBUG) {
                                 Slog.i(
                                         TAG,
                                         addUserIdToLogMessage(
@@ -1169,7 +1062,7 @@
         if (!packageNames.isEmpty()) {
             String msg = "Stale backup journals: Scheduled " + packageNames.size()
                     + " package(s) total";
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 msg += ": " + packageNames;
             }
             Slog.i(TAG, addUserIdToLogMessage(mUserId, msg));
@@ -1209,7 +1102,7 @@
     public void recordInitPending(
             boolean isPending, String transportName, String transportDirName) {
         synchronized (mQueueLock) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(
                         TAG,
                         addUserIdToLogMessage(
@@ -1276,20 +1169,11 @@
     }
 
     private void onTransportRegistered(String transportName, String transportDirName) {
-        if (DEBUG) {
-            long timeMs = SystemClock.elapsedRealtime() - mRegisterTransportsRequestedTime;
-            Slog.d(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId,
-                            "Transport "
-                                    + transportName
-                                    + " registered "
-                                    + timeMs
-                                    + "ms after first request (delay = "
-                                    + INITIALIZATION_DELAY_MILLIS
-                                    + "ms)"));
-        }
+        long timeMs = SystemClock.elapsedRealtime() - mRegisterTransportsRequestedTime;
+        Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                "Transport " + transportName + " registered " + timeMs
+                        + "ms after first request (delay = " + INITIALIZATION_DELAY_MILLIS
+                        + "ms)"));
 
         File stateDir = new File(mBaseStateDir, transportDirName);
         stateDir.mkdirs();
@@ -1313,7 +1197,7 @@
      */
     private BroadcastReceiver mPackageTrackingReceiver = new BroadcastReceiver() {
         public void onReceive(Context context, Intent intent) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.d(TAG, addUserIdToLogMessage(mUserId, "Received broadcast " + intent));
             }
 
@@ -1344,7 +1228,7 @@
                             intent.getStringArrayExtra(
                                     Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
 
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.i(
                                 TAG,
                                 addUserIdToLogMessage(
@@ -1416,13 +1300,8 @@
                         mBackupHandler.post(
                                 () -> mTransportManager.onPackageAdded(packageName));
                     } catch (NameNotFoundException e) {
-                        if (DEBUG) {
-                            Slog.w(
-                                    TAG,
-                                    addUserIdToLogMessage(
-                                            mUserId,
-                                            "Can't resolve new app " + packageName));
-                        }
+                        Slog.w(TAG, addUserIdToLogMessage(mUserId,
+                                "Can't resolve new app " + packageName));
                     }
                 }
 
@@ -1454,7 +1333,7 @@
         // Look for apps that define the android:backupAgent attribute
         List<PackageInfo> targetApps = allAgentPackages();
         if (packageNames != null) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(
                         TAG,
                         addUserIdToLogMessage(
@@ -1464,7 +1343,7 @@
                 addPackageParticipantsLockedInner(packageName, targetApps);
             }
         } else {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, addUserIdToLogMessage(mUserId, "addPackageParticipantsLocked: all"));
             }
             addPackageParticipantsLockedInner(null, targetApps);
@@ -1473,7 +1352,7 @@
 
     private void addPackageParticipantsLockedInner(String packageName,
             List<PackageInfo> targetPkgs) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(
                     TAG,
                     addUserIdToLogMessage(
@@ -1489,10 +1368,10 @@
                     mBackupParticipants.put(uid, set);
                 }
                 set.add(pkg.packageName);
-                if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "Agent found; added"));
+                if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "Agent found; added"));
 
                 // Schedule a backup for it on general principles
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.i(
                             TAG,
                             addUserIdToLogMessage(
@@ -1512,7 +1391,7 @@
             return;
         }
 
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(
                     TAG,
                     addUserIdToLogMessage(
@@ -1528,7 +1407,7 @@
             if (set != null && set.contains(pkg)) {
                 removePackageFromSetLocked(set, pkg);
                 if (set.isEmpty()) {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.v(
                                 TAG,
                                 addUserIdToLogMessage(
@@ -1549,7 +1428,7 @@
             // Note that we deliberately leave it 'known' in the "ever backed up"
             // bookkeeping so that its current-dataset data will be retrieved
             // if the app is subsequently reinstalled
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(
                         TAG,
                         addUserIdToLogMessage(mUserId, "  removing participant " + packageName));
@@ -1627,15 +1506,13 @@
                 af.writeInt(-1);
             } else {
                 af.writeInt(mAncestralPackages.size());
-                if (DEBUG) {
-                    Slog.v(
-                            TAG,
-                            addUserIdToLogMessage(
-                                    mUserId, "Ancestral packages:  " + mAncestralPackages.size()));
-                }
+                Slog.d(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId, "Ancestral packages:  " + mAncestralPackages.size()));
                 for (String pkgName : mAncestralPackages) {
                     af.writeUTF(pkgName);
-                    if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "   " + pkgName));
+                    if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "   " + pkgName));
                 }
             }
         } catch (IOException e) {
@@ -1686,7 +1563,7 @@
             }
 
             if (!shouldClearData) {
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.i(
                             TAG,
                             addUserIdToLogMessage(
@@ -1761,7 +1638,7 @@
         long token = mAncestralToken;
         synchronized (mQueueLock) {
             if (mCurrentToken != 0 && mProcessedPackagesJournal.hasBeenProcessed(packageName)) {
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.i(
                             TAG,
                             addUserIdToLogMessage(
@@ -1770,7 +1647,7 @@
                 token = mCurrentToken;
             }
         }
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, addUserIdToLogMessage(mUserId, "getAvailableRestoreToken() == " + token));
         }
         return token;
@@ -1885,7 +1762,7 @@
 
         EventLog.writeEvent(EventLogTags.BACKUP_REQUESTED, packages.length, kvBackupList.size(),
                 fullBackupList.size());
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(
                     TAG,
                     addUserIdToLogMessage(
@@ -1909,7 +1786,7 @@
     /** Cancel all running backups. */
     public void cancelBackups() {
         mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "cancelBackups");
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, addUserIdToLogMessage(mUserId, "cancelBackups() called."));
         }
         final long oldToken = Binder.clearCallingIdentity();
@@ -1946,7 +1823,7 @@
                                     + operationType));
             return;
         }
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(
                     TAG,
                     addUserIdToLogMessage(
@@ -2025,12 +1902,8 @@
                 FullBackupJob.schedule(mUserId, mContext, latency,
                         /* userBackupManagerService */ this);
             } else {
-                if (DEBUG_SCHEDULING) {
-                    Slog.i(
-                            TAG,
-                            addUserIdToLogMessage(
-                                    mUserId, "Full backup queue empty; not scheduling"));
-                }
+                Slog.i(TAG,
+                        addUserIdToLogMessage(mUserId, "Full backup queue empty; not scheduling"));
             }
         }
     }
@@ -2098,13 +1971,8 @@
             File stateDir = new File(mBaseStateDir, transportDirName);
             File pmState = new File(stateDir, PACKAGE_MANAGER_SENTINEL);
             if (pmState.length() <= 0) {
-                if (DEBUG) {
-                    Slog.i(
-                            TAG,
-                            addUserIdToLogMessage(
-                                    mUserId,
-                                    "Full backup requested but dataset not yet initialized"));
-                }
+                Slog.i(TAG, addUserIdToLogMessage(mUserId,
+                        "Full backup requested but dataset not yet initialized"));
                 return false;
             }
         } catch (Exception e) {
@@ -2143,7 +2011,7 @@
             // Backups are globally disabled, so don't proceed.  We also don't reschedule
             // the job driving automatic backups; that job will be scheduled again when
             // the user enables backup.
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, addUserIdToLogMessage(mUserId, "beginFullBackup but enabled=" + mEnabled
                         + " setupComplete=" + mSetupComplete + "; ignoring"));
             }
@@ -2155,22 +2023,14 @@
         final PowerSaveState result =
                 mPowerManager.getPowerSaveState(ServiceType.FULL_BACKUP);
         if (result.batterySaverEnabled) {
-            if (DEBUG) {
-                Slog.i(
-                        TAG,
-                        addUserIdToLogMessage(
-                                mUserId, "Deferring scheduled full backups in battery saver mode"));
-            }
+            Slog.i(TAG, addUserIdToLogMessage(mUserId,
+                    "Deferring scheduled full backups in battery saver mode"));
             FullBackupJob.schedule(mUserId, mContext, keyValueBackupInterval,
                     /* userBackupManagerService */ this);
             return false;
         }
 
-        if (DEBUG_SCHEDULING) {
-            Slog.i(
-                    TAG,
-                    addUserIdToLogMessage(mUserId, "Beginning scheduled full backup operation"));
-        }
+        Slog.i(TAG, addUserIdToLogMessage(mUserId, "Beginning scheduled full backup operation"));
 
         // Great; we're able to run full backup jobs now.  See if we have any work to do.
         synchronized (mQueueLock) {
@@ -2193,12 +2053,8 @@
                 // have emptied the queue.
                 if (mFullBackupQueue.size() == 0) {
                     // no work to do so just bow out
-                    if (DEBUG) {
-                        Slog.i(
-                                TAG,
-                                addUserIdToLogMessage(
-                                        mUserId, "Backup queue empty; doing nothing"));
-                    }
+                    Slog.i(TAG,
+                            addUserIdToLogMessage(mUserId, "Backup queue empty; doing nothing"));
                     runBackup = false;
                     break;
                 }
@@ -2207,7 +2063,7 @@
 
                 String transportName = mTransportManager.getCurrentTransportName();
                 if (!fullBackupAllowable(transportName)) {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.i(
                                 TAG,
                                 addUserIdToLogMessage(
@@ -2226,7 +2082,7 @@
                     runBackup = (timeSinceRun >= fullBackupInterval);
                     if (!runBackup) {
                         // It's too early to back up the next thing in the queue, so bow out
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.i(
                                     TAG,
                                     addUserIdToLogMessage(
@@ -2245,7 +2101,7 @@
                             // The head app isn't supposed to get full-data backups [any more];
                             // so we cull it and force a loop around to consider the new head
                             // app.
-                            if (MORE_DEBUG) {
+                            if (DEBUG) {
                                 Slog.i(
                                         TAG,
                                         addUserIdToLogMessage(
@@ -2269,17 +2125,11 @@
                             final long nextEligible = System.currentTimeMillis()
                                     + BUSY_BACKOFF_MIN_MILLIS
                                     + mTokenGenerator.nextInt(BUSY_BACKOFF_FUZZ);
-                            if (DEBUG_SCHEDULING) {
-                                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-                                Slog.i(
-                                        TAG,
-                                        addUserIdToLogMessage(
-                                                mUserId,
-                                                "Full backup time but "
-                                                        + entry.packageName
-                                                        + " is busy; deferring to "
-                                                        + sdf.format(new Date(nextEligible))));
-                            }
+                            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                            Slog.i(TAG, addUserIdToLogMessage(mUserId,
+                                    "Full backup time but " + entry.packageName
+                                            + " is busy; deferring to " + sdf.format(
+                                                new Date(nextEligible))));
                             // This relocates the app's entry from the head of the queue to
                             // its order-appropriate position further down, so upon looping
                             // a new candidate will be considered at the head.
@@ -2319,14 +2169,9 @@
             }
 
             if (!runBackup) {
-                if (DEBUG_SCHEDULING) {
-                    Slog.i(
-                            TAG,
-                            addUserIdToLogMessage(
-                                    mUserId,
-                                    "Nothing pending full backup or failed to start the "
-                                            + "operation; rescheduling +" + latency));
-                }
+                Slog.i(TAG, addUserIdToLogMessage(mUserId,
+                        "Nothing pending full backup or failed to start the "
+                                + "operation; rescheduling +" + latency));
                 final long deferTime = latency;     // pin for the closure
                 FullBackupJob.schedule(mUserId, mContext, deferTime,
                         /* userBackupManagerService */ this);
@@ -2360,12 +2205,7 @@
                     }
                 }
                 if (pftbt != null) {
-                    if (DEBUG_SCHEDULING) {
-                        Slog.i(
-                                TAG,
-                                addUserIdToLogMessage(
-                                        mUserId, "Telling running backup to stop"));
-                    }
+                    Slog.i(TAG, addUserIdToLogMessage(mUserId, "Telling running backup to stop"));
                     pftbt.handleCancel(true);
                 }
             }
@@ -2376,7 +2216,7 @@
     /** Used by both incremental and full restore to restore widget data. */
     public void restoreWidgetData(String packageName, byte[] widgetData) {
         // Apply the restored widget state and generate the ID update for the app
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, addUserIdToLogMessage(mUserId, "Incorporating restored widget data"));
         }
         AppWidgetBackupBridge.restoreWidgetState(packageName, widgetData, mUserId);
@@ -2412,7 +2252,7 @@
                 // one already there, then overwrite it, but no harm done.
                 BackupRequest req = new BackupRequest(packageName);
                 if (mPendingBackups.put(packageName, req) == null) {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(
                                 TAG,
                                 addUserIdToLogMessage(
@@ -2495,7 +2335,7 @@
     public void initializeTransports(String[] transportNames, IBackupObserver observer) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "initializeTransport");
-        Slog.v(
+        Slog.d(
                 TAG,
                 addUserIdToLogMessage(
                         mUserId, "initializeTransport(): " + Arrays.asList(transportNames)));
@@ -2517,7 +2357,7 @@
     public void setAncestralSerialNumber(long ancestralSerialNumber) {
         mContext.enforceCallingPermission(android.Manifest.permission.BACKUP,
                 "setAncestralSerialNumber");
-        Slog.v(
+        Slog.d(
                 TAG,
                 addUserIdToLogMessage(
                         mUserId, "Setting ancestral work profile id to " + ancestralSerialNumber));
@@ -2571,13 +2411,8 @@
 
     /** Clear the given package's backup data from the current transport. */
     public void clearBackupData(String transportName, String packageName) {
-        if (DEBUG) {
-            Slog.v(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId,
-                            "clearBackupData() of " + packageName + " on " + transportName));
-        }
+        Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                "clearBackupData() of " + packageName + " on " + transportName));
 
         PackageInfo info;
         try {
@@ -2601,7 +2436,7 @@
         } else {
             // a caller with full permission can ask to back up any participating app
             // !!! TODO: allow data-clear of ANY app?
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(
                         TAG,
                         addUserIdToLogMessage(
@@ -2612,7 +2447,7 @@
 
         if (apps.contains(packageName)) {
             // found it; fire off the clear request
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(
                         TAG,
                         addUserIdToLogMessage(mUserId, "Found the app - running clear process"));
@@ -2657,25 +2492,19 @@
             final PowerSaveState result =
                     mPowerManager.getPowerSaveState(ServiceType.KEYVALUE_BACKUP);
             if (result.batterySaverEnabled) {
-                if (DEBUG) {
-                    Slog.v(
-                            TAG,
-                            addUserIdToLogMessage(
-                                    mUserId, "Not running backup while in battery save mode"));
-                }
+                Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                        "Not running backup while in battery save mode"));
                 // Try again in several hours.
                 KeyValueBackupJob.schedule(mUserId, mContext,
                         /* userBackupManagerService */ this);
             } else {
-                if (DEBUG) {
-                    Slog.v(TAG, addUserIdToLogMessage(mUserId, "Scheduling immediate backup pass"));
-                }
+                Slog.d(TAG, addUserIdToLogMessage(mUserId, "Scheduling immediate backup pass"));
 
                 synchronized (getQueueLock()) {
                     if (getPendingInits().size() > 0) {
                         // If there are pending init operations, we process those and then settle
                         // into the usual periodic backup schedule.
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.v(
                                     TAG,
                                     addUserIdToLogMessage(
@@ -2749,26 +2578,11 @@
                 return;
             }
 
-            if (DEBUG) {
-                Slog.v(
-                        TAG,
-                        addUserIdToLogMessage(
-                                mUserId,
-                                "Requesting backup: apks="
-                                        + includeApks
-                                        + " obb="
-                                        + includeObbs
-                                        + " shared="
-                                        + includeShared
-                                        + " all="
-                                        + doAllApps
-                                        + " system="
-                                        + includeSystem
-                                        + " includekeyvalue="
-                                        + doKeyValue
-                                        + " pkgs="
-                                        + Arrays.toString(pkgList)));
-            }
+            Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                    "Requesting backup: apks=" + includeApks + " obb=" + includeObbs + " shared="
+                            + includeShared + " all=" + doAllApps + " system=" + includeSystem
+                            + " includekeyvalue=" + doKeyValue + " pkgs=" + Arrays.toString(
+                            pkgList)));
             Slog.i(TAG, addUserIdToLogMessage(mUserId, "Beginning adb backup..."));
 
             BackupEligibilityRules eligibilityRules = getEligibilityRulesForOperation(
@@ -2782,11 +2596,7 @@
             }
 
             // start up the confirmation UI
-            if (DEBUG) {
-                Slog.d(
-                        TAG,
-                        addUserIdToLogMessage(mUserId, "Starting backup confirmation UI"));
-            }
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Starting backup confirmation UI"));
             if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) {
                 Slog.e(
                         TAG,
@@ -2804,9 +2614,7 @@
             startConfirmationTimeout(token, params);
 
             // wait for the backup to be performed
-            if (DEBUG) {
-                Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for backup completion..."));
-            }
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for backup completion..."));
             waitForCompletion(params);
         } finally {
             try {
@@ -2841,9 +2649,7 @@
                             mUserId,
                             "Full backup not currently possible -- key/value backup not yet run?"));
         } else {
-            if (DEBUG) {
-                Slog.d(TAG, addUserIdToLogMessage(mUserId, "fullTransportBackup()"));
-            }
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "fullTransportBackup()"));
 
             final long oldId = Binder.clearCallingIdentity();
             try {
@@ -2886,9 +2692,7 @@
             }
         }
 
-        if (DEBUG) {
-            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Done with full transport backup."));
-        }
+        Slog.d(TAG, addUserIdToLogMessage(mUserId, "Done with full transport backup."));
     }
 
     /**
@@ -2922,12 +2726,8 @@
             }
 
             // start up the confirmation UI
-            if (DEBUG) {
-                Slog.d(
-                        TAG,
-                        addUserIdToLogMessage(
-                                mUserId, "Starting restore confirmation UI, token=" + token));
-            }
+            Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                    "Starting restore confirmation UI, token=" + token));
             if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) {
                 Slog.e(
                         TAG,
@@ -2945,9 +2745,7 @@
             startConfirmationTimeout(token, params);
 
             // wait for the restore to be performed
-            if (DEBUG) {
-                Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for restore completion..."));
-            }
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for restore completion..."));
             waitForCompletion(params);
         } finally {
             try {
@@ -3022,7 +2820,7 @@
     }
 
     private void startConfirmationTimeout(int token, AdbParams params) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.d(TAG, addUserIdToLogMessage(mUserId, "Posting conf timeout msg after "
                     + TIMEOUT_FULL_CONFIRMATION + " millis"));
         }
@@ -3055,13 +2853,8 @@
      */
     public void acknowledgeAdbBackupOrRestore(int token, boolean allow,
             String curPassword, String encPpassword, IFullBackupRestoreObserver observer) {
-        if (DEBUG) {
-            Slog.d(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId,
-                            "acknowledgeAdbBackupOrRestore : token=" + token + " allow=" + allow));
-        }
+        Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                "acknowledgeAdbBackupOrRestore : token=" + token + " allow=" + allow));
 
         // TODO: possibly require not just this signature-only permission, but even
         // require that the specific designated confirmation-UI app uid is the caller?
@@ -3088,7 +2881,7 @@
 
                         params.encryptPassword = encPpassword;
 
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.d(
                                     TAG,
                                     addUserIdToLogMessage(
@@ -3198,7 +2991,7 @@
                 scheduleNextFullBackupJob(0);
             } else if (!enable) {
                 // No longer enabled, so stop running backups
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.i(TAG, addUserIdToLogMessage(mUserId, "Opting out of backup"));
                 }
 
@@ -3285,7 +3078,7 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "getCurrentTransport");
         String currentTransport = mTransportManager.getCurrentTransportName();
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(
                     TAG,
                     addUserIdToLogMessage(
@@ -3429,7 +3222,7 @@
         final long oldId = Binder.clearCallingIdentity();
         try {
             if (!mTransportManager.isTransportRegistered(transportName)) {
-                Slog.v(
+                Slog.d(
                         TAG,
                         addUserIdToLogMessage(
                                 mUserId,
@@ -3441,7 +3234,7 @@
 
             String previousTransportName = mTransportManager.selectTransport(transportName);
             updateStateForTransport(transportName);
-            Slog.v(
+            Slog.d(
                     TAG,
                     addUserIdToLogMessage(
                             mUserId,
@@ -3467,7 +3260,7 @@
         final long oldId = Binder.clearCallingIdentity();
         try {
             String transportString = transportComponent.flattenToShortString();
-            Slog.v(
+            Slog.d(
                     TAG,
                     addUserIdToLogMessage(
                             mUserId,
@@ -3562,7 +3355,7 @@
                 "getConfigurationIntent");
         try {
             Intent intent = mTransportManager.getTransportConfigurationIntent(transportName);
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.d(
                         TAG,
                         addUserIdToLogMessage(
@@ -3595,7 +3388,7 @@
 
         try {
             String string = mTransportManager.getTransportCurrentDestinationString(transportName);
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.d(
                         TAG,
                         addUserIdToLogMessage(
@@ -3619,7 +3412,7 @@
 
         try {
             Intent intent = mTransportManager.getTransportDataManagementIntent(transportName);
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.d(
                         TAG,
                         addUserIdToLogMessage(
@@ -3646,7 +3439,7 @@
 
         try {
             CharSequence label = mTransportManager.getTransportDataManagementLabel(transportName);
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.d(
                         TAG,
                         addUserIdToLogMessage(
@@ -3682,20 +3475,11 @@
         boolean skip = false;
 
         long restoreSet = getAvailableRestoreToken(packageName);
-        if (DEBUG) {
-            Slog.v(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId,
-                            "restoreAtInstall pkg="
-                                    + packageName
-                                    + " token="
-                                    + Integer.toHexString(token)
-                                    + " restoreSet="
-                                    + Long.toHexString(restoreSet)));
-        }
+        Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                "restoreAtInstall pkg=" + packageName + " token=" + Integer.toHexString(token)
+                        + " restoreSet=" + Long.toHexString(restoreSet)));
         if (restoreSet == 0) {
-            if (MORE_DEBUG) Slog.i(TAG, addUserIdToLogMessage(mUserId, "No restore set"));
+            if (DEBUG) Slog.i(TAG, addUserIdToLogMessage(mUserId, "No restore set"));
             skip = true;
         }
 
@@ -3705,7 +3489,7 @@
         TransportConnection transportConnection =
                 mTransportManager.getCurrentTransportClient("BMS.restoreAtInstall()");
         if (transportConnection == null) {
-            if (DEBUG) Slog.w(TAG, addUserIdToLogMessage(mUserId, "No transport client"));
+            Slog.w(TAG, addUserIdToLogMessage(mUserId, "No transport client"));
             skip = true;
         } else if (Flags.enableIncreasedBmmLoggingForRestoreAtInstall()) {
             try {
@@ -3729,12 +3513,8 @@
         }
 
         if (!mAutoRestore) {
-            if (DEBUG) {
-                Slog.w(
-                        TAG,
-                        addUserIdToLogMessage(
-                                mUserId, "Non-restorable state: auto=" + mAutoRestore));
-            }
+            Slog.w(TAG,
+                    addUserIdToLogMessage(mUserId, "Non-restorable state: auto=" + mAutoRestore));
             skip = true;
         }
 
@@ -3751,7 +3531,7 @@
                     mWakelock.release();
                 };
 
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.d(
                             TAG,
                             addUserIdToLogMessage(mUserId, "Restore at install of " + packageName));
@@ -3796,7 +3576,7 @@
             }
 
             // Tell the PackageManager to proceed with the post-install handling for this package.
-            if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "Finishing install immediately"));
+            Slog.d(TAG, addUserIdToLogMessage(mUserId, "Finishing install immediately"));
             try {
                 mPackageManagerBinder.finishPackageInstall(token, false);
             } catch (RemoteException e) { /* can't happen */ }
@@ -3812,13 +3592,8 @@
 
     /** Hand off a restore session. */
     public IRestoreSession beginRestoreSession(String packageName, String transport) {
-        if (DEBUG) {
-            Slog.v(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId,
-                            "beginRestoreSession: pkg=" + packageName + " transport=" + transport));
-        }
+        Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                "beginRestoreSession: pkg=" + packageName + " transport=" + transport));
 
         boolean needPermission = true;
         if (transport == null) {
@@ -3849,13 +3624,8 @@
             mContext.enforceCallingOrSelfPermission(
                     android.Manifest.permission.BACKUP, "beginRestoreSession");
         } else {
-            if (DEBUG) {
-                Slog.d(
-                        TAG,
-                        addUserIdToLogMessage(
-                                mUserId,
-                                "restoring self on current transport; no permission needed"));
-            }
+            Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                    "restoring self on current transport; no permission needed"));
         }
 
         int backupDestination;
@@ -3905,12 +3675,8 @@
             if (currentSession != mActiveRestoreSession) {
                 Slog.e(TAG, addUserIdToLogMessage(mUserId, "ending non-current restore session"));
             } else {
-                if (DEBUG) {
-                    Slog.v(
-                            TAG,
-                            addUserIdToLogMessage(
-                                    mUserId, "Clearing restore session and halting timeout"));
-                }
+                Slog.d(TAG, addUserIdToLogMessage(mUserId,
+                        "Clearing restore session and halting timeout"));
                 mActiveRestoreSession = null;
                 mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
             }
diff --git a/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java b/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java
index d13f711..1cd8d81 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java
@@ -1,6 +1,6 @@
 package com.android.server.backup.fullbackup;
 
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_VERSION;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_VERSION;
@@ -261,7 +261,7 @@
                 new Environment.UserEnvironment(userId);
         File obbDir = userEnv.buildExternalStorageAppObbDirs(packageInfo.packageName)[0];
         if (obbDir != null) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath());
             }
             File[] obbFiles = obbDir.listFiles();
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
index cf617a5..ebb1194 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
@@ -17,7 +17,6 @@
 package com.android.server.backup.fullbackup;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
@@ -111,7 +110,7 @@
                         shouldWriteApk(mPackage.applicationInfo, mIncludeApks, isSharedStorage);
 
                 if (!isSharedStorage) {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, "Writing manifest for " + packageName);
                     }
 
@@ -137,9 +136,7 @@
                     appMetadataBackupWriter.backupObb(mUserId, mPackage);
                 }
 
-                if (DEBUG) {
-                    Slog.d(TAG, "Calling doFullBackup() on " + packageName);
-                }
+                Slog.d(TAG, "Calling doFullBackup() on " + packageName);
 
                 long timeout =
                         isSharedStorage
@@ -216,14 +213,14 @@
 
     public int preflightCheck() throws RemoteException {
         if (mPreflightHook == null) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, "No preflight check");
             }
             return BackupTransport.TRANSPORT_OK;
         }
         if (initializeAgent()) {
             int result = mPreflightHook.preflightFullBackup(mPkg, mAgent);
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, "preflight returned " + result);
             }
 
@@ -262,7 +259,7 @@
                 if (!backupManagerService.waitUntilOperationComplete(mOpToken)) {
                     Slog.e(TAG, "Full backup failed on package " + mPkg.packageName);
                 } else {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, "Full package backup success: " + mPkg.packageName);
                     }
                     result = BackupTransport.TRANSPORT_OK;
@@ -310,7 +307,7 @@
 
     private boolean initializeAgent() {
         if (mAgent == null) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.d(TAG, "Binding to full backup agent : " + mPkg.packageName);
             }
             mAgent =
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java
index be6ac26..93499b9 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java
@@ -16,7 +16,7 @@
 
 package com.android.server.backup.fullbackup;
 
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 
 import android.app.backup.IBackupManager;
@@ -58,7 +58,7 @@
     }
 
     public void establish() {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, "Initiating bind of OBB service on " + this);
         }
         Intent obbIntent = new Intent().setComponent(new ComponentName(
@@ -124,14 +124,14 @@
     private void waitForConnection() {
         synchronized (this) {
             while (mService == null) {
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.i(TAG, "...waiting for OBB service binding...");
                 }
                 try {
                     this.wait();
                 } catch (InterruptedException e) { /* never interrupted */ }
             }
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, "Connected to OBB service; continuing");
             }
         }
@@ -141,7 +141,7 @@
     public void onServiceConnected(ComponentName name, IBinder service) {
         synchronized (this) {
             mService = IObbBackupService.Stub.asInterface(service);
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, "OBB service connection " + mService + " connected on " + this);
             }
             this.notifyAll();
@@ -152,7 +152,7 @@
     public void onServiceDisconnected(ComponentName name) {
         synchronized (this) {
             mService = null;
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, "OBB service connection disconnected on " + this);
             }
             this.notifyAll();
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
index 0ba0d71..0d4364e 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
@@ -17,7 +17,6 @@
 package com.android.server.backup.fullbackup;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
@@ -121,7 +120,7 @@
         } else {
             mEncryptPassword = encryptPassword;
         }
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.w(TAG, "Encrypting backup with passphrase=" + mEncryptPassword);
         }
         mCompress = doCompress;
@@ -265,7 +264,7 @@
             List<String> pkgs =
                     AppWidgetBackupBridge.getWidgetParticipants(UserHandle.USER_SYSTEM);
             if (pkgs != null) {
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.i(TAG, "Adding widget participants to backup set:");
                     StringBuilder sb = new StringBuilder(128);
                     sb.append("   ");
@@ -297,16 +296,12 @@
             if (!mBackupEligibilityRules.appIsEligibleForBackup(pkg.applicationInfo)
                     || mBackupEligibilityRules.appIsStopped(pkg.applicationInfo)) {
                 iter.remove();
-                if (DEBUG) {
-                    Slog.i(TAG, "Package " + pkg.packageName
+                Slog.i(TAG, "Package " + pkg.packageName
                             + " is not eligible for backup, removing.");
-                }
             } else if (mBackupEligibilityRules.appIsKeyValueOnly(pkg)) {
                 iter.remove();
-                if (DEBUG) {
-                    Slog.i(TAG, "Package " + pkg.packageName
+                Slog.i(TAG, "Package " + pkg.packageName
                             + " is key-value.");
-                }
                 keyValueBackupQueue.add(pkg);
             }
         }
@@ -326,9 +321,7 @@
             // Verify that the given password matches the currently-active
             // backup password, if any
             if (!mUserBackupManagerService.backupPasswordMatches(mCurrentPassword)) {
-                if (DEBUG) {
-                    Slog.w(TAG, "Backup password mismatch; aborting");
-                }
+                Slog.w(TAG, "Backup password mismatch; aborting");
                 return;
             }
 
@@ -402,10 +395,8 @@
             int N = backupQueue.size();
             for (int i = 0; i < N; i++) {
                 pkg = backupQueue.get(i);
-                if (DEBUG) {
-                    Slog.i(TAG, "--- Performing full backup for package " + pkg.packageName
+                Slog.i(TAG, "--- Performing full backup for package " + pkg.packageName
                             + " ---");
-                }
                 final boolean isSharedStorage =
                         pkg.packageName.equals(
                                 SHARED_BACKUP_AGENT_PACKAGE);
@@ -441,10 +432,8 @@
             // And for key-value backup if enabled
             if (mKeyValue) {
                 for (PackageInfo keyValuePackage : keyValueBackupQueue) {
-                    if (DEBUG) {
-                        Slog.i(TAG, "--- Performing key-value backup for package "
+                    Slog.i(TAG, "--- Performing key-value backup for package "
                                 + keyValuePackage.packageName + " ---");
-                    }
                     KeyValueAdbBackupEngine kvBackupEngine =
                             new KeyValueAdbBackupEngine(out, keyValuePackage,
                                     mUserBackupManagerService,
@@ -478,10 +467,8 @@
             }
             sendEndBackup();
             obbConnection.tearDown();
-            if (DEBUG) {
-                Slog.d(TAG, "Full backup pass complete.");
-            }
-            mUserBackupManagerService.getWakelock().release();
+            Slog.d(TAG, "Full backup pass complete.");
+            mUserBackupManagerService.getWakeLock().release();
         }
     }
 
@@ -499,9 +486,7 @@
     @Override
     public void handleCancel(boolean cancelAll) {
         final PackageInfo target = mCurrentTarget;
-        if (DEBUG) {
-            Slog.w(TAG, "adb backup cancel of " + target);
-        }
+        Slog.w(TAG, "adb backup cancel of " + target);
         if (target != null) {
             mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent(
                     target.applicationInfo, /* allowKill= */ true);
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index 990c941..bd34f33 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -19,8 +19,6 @@
 import static android.app.backup.BackupAnnotations.OperationType.BACKUP;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 
 import android.annotation.Nullable;
 import android.app.IBackupAgent;
@@ -201,9 +199,7 @@
         mBackupEligibilityRules = backupEligibilityRules;
 
         if (backupManagerService.isBackupOperationInProgress()) {
-            if (DEBUG) {
-                Slog.d(TAG, "Skipping full backup. A backup is already in progress.");
-            }
+            Slog.d(TAG, "Skipping full backup. A backup is already in progress.");
             mCancelAll = true;
             return;
         }
@@ -219,7 +215,7 @@
                     // that run as system-domain uids but do not define their own backup agents,
                     // as well as any explicit mention of the 'special' shared-storage agent
                     // package (we handle that one at the end).
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, "Ignoring ineligible package " + pkg);
                     }
                     mBackupManagerMonitorEventSender.monitorEvent(
@@ -233,7 +229,7 @@
                 } else if (!mBackupEligibilityRules.appGetsFullBackup(info)) {
                     // Cull any packages that are found in the queue but now aren't supposed
                     // to get full-data backup operations.
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, "Ignoring full-data backup of key/value participant "
                                 + pkg);
                     }
@@ -249,7 +245,7 @@
                     // Cull any packages in the 'stopped' state: they've either just been
                     // installed or have explicitly been force-stopped by the user.  In both
                     // cases we do not want to launch them for backup.
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, "Ignoring stopped package " + pkg);
                     }
                     mBackupManagerMonitorEventSender.monitorEvent(
@@ -346,12 +342,10 @@
             if (!mUserBackupManagerService.isEnabled()
                     || !mUserBackupManagerService.isSetupComplete()) {
                 // Backups are globally disabled, so don't proceed.
-                if (DEBUG) {
-                    Slog.i(TAG, "full backup requested but enabled=" + mUserBackupManagerService
-                            .isEnabled()
-                            + " setupComplete=" + mUserBackupManagerService.isSetupComplete()
-                            + "; ignoring");
-                }
+                Slog.i(TAG,
+                        "full backup requested but enabled=" + mUserBackupManagerService.isEnabled()
+                                + " setupComplete=" + mUserBackupManagerService.isSetupComplete()
+                                + "; ignoring");
                 int monitoringEvent;
                 if (mUserBackupManagerService.isSetupComplete()) {
                     monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED;
@@ -405,10 +399,8 @@
                 mBackupRunner = null;
                 PackageInfo currentPackage = mPackages.get(i);
                 String packageName = currentPackage.packageName;
-                if (DEBUG) {
-                    Slog.i(TAG, "Initiating full-data transport backup of " + packageName
-                            + " token: " + mCurrentOpToken);
-                }
+                Slog.i(TAG, "Initiating full-data transport backup of " + packageName + " token: "
+                        + mCurrentOpToken);
                 EventLog.writeEvent(EventLogTags.FULL_BACKUP_PACKAGE, packageName);
 
                 transportPipes = ParcelFileDescriptor.createPipe();
@@ -462,7 +454,7 @@
                     final long preflightResult = mBackupRunner.getPreflightResultBlocking();
                     // Preflight result is negative if some error happened on preflight.
                     if (preflightResult < 0) {
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.d(TAG, "Backup error after preflight of package "
                                     + packageName + ": " + preflightResult
                                     + ", not running backup.");
@@ -479,7 +471,7 @@
                         int nRead = 0;
                         do {
                             nRead = in.read(buffer);
-                            if (MORE_DEBUG) {
+                            if (DEBUG) {
                                 Slog.v(TAG, "in.read(buffer) from app: " + nRead);
                             }
                             if (nRead > 0) {
@@ -549,12 +541,12 @@
                             backupPackageStatus = backupRunnerResult;
                         }
                     } else {
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.i(TAG, "Transport-level failure; cancelling agent work");
                         }
                     }
 
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.i(TAG, "Done delivering backup data: result="
                                 + backupPackageStatus);
                     }
@@ -567,10 +559,7 @@
                     // Also ask the transport how long it wants us to wait before
                     // moving on to the next package, if any.
                     backoff = transport.requestFullBackupTime();
-                    if (DEBUG_SCHEDULING) {
-                        Slog.i(TAG, "Transport suggested backoff=" + backoff);
-                    }
-
+                    Slog.i(TAG, "Transport suggested backoff=" + backoff);
                 }
 
                 // Roll this package to the end of the backup queue if we're
@@ -581,13 +570,9 @@
                 }
 
                 if (backupPackageStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
-                    BackupObserverUtils
-                            .sendBackupOnPackageResult(mBackupObserver, packageName,
-                                    BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
-                    if (DEBUG) {
-                        Slog.i(TAG, "Transport rejected backup of " + packageName
-                                + ", skipping");
-                    }
+                    BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, packageName,
+                            BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
+                    Slog.i(TAG, "Transport rejected backup of " + packageName + ", skipping");
                     EventLog.writeEvent(EventLogTags.FULL_BACKUP_AGENT_FAILURE, packageName,
                             "transport rejected");
                     // This failure state can come either a-priori from the transport, or
@@ -602,11 +587,8 @@
                     BackupObserverUtils
                             .sendBackupOnPackageResult(mBackupObserver, packageName,
                                     BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
-                    if (DEBUG) {
-                        Slog.i(TAG, "Transport quota exceeded for package: " + packageName);
-                        EventLog.writeEvent(EventLogTags.FULL_BACKUP_QUOTA_EXCEEDED,
-                                packageName);
-                    }
+                    Slog.i(TAG, "Transport quota exceeded for package: " + packageName);
+                    EventLog.writeEvent(EventLogTags.FULL_BACKUP_QUOTA_EXCEEDED, packageName);
                     mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent(
                             currentPackage.applicationInfo, /* allowKill= */ true);
                     // Do nothing, clean up, and continue looping.
@@ -675,9 +657,7 @@
                 backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED;
             }
 
-            if (DEBUG) {
-                Slog.i(TAG, "Full backup completed with status: " + backupRunStatus);
-            }
+            Slog.i(TAG, "Full backup completed with status: " + backupRunStatus);
             BackupObserverUtils.sendBackupFinished(mBackupObserver, backupRunStatus);
 
             cleanUpPipes(transportPipes);
@@ -708,7 +688,7 @@
                     .getBackupAgentConnectionManager().clearNoRestrictedModePackages();
 
             Slog.i(TAG, "Full data backup pass finished.");
-            mUserBackupManagerService.getWakelock().release();
+            mUserBackupManagerService.getWakeLock().release();
         }
     }
 
@@ -781,7 +761,7 @@
             try {
                 mUserBackupManagerService.prepareOperationTimeout(
                         mCurrentOpToken, fullBackupAgentTimeoutMillis, this, OpType.BACKUP_WAIT);
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.d(TAG, "Preflighting full payload of " + pkg.packageName);
                 }
                 agent.doMeasureFullBackup(mQuota, mCurrentOpToken,
@@ -799,7 +779,7 @@
                 if (totalSize < 0) {
                     return (int) totalSize;
                 }
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.v(TAG, "Got preflight response; size=" + totalSize);
                 }
 
@@ -807,7 +787,7 @@
                         mTransportConnection.connectOrThrow("PFTBT$SPBP.preflightFullBackup()");
                 result = transport.checkFullBackupSize(totalSize);
                 if (result == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.d(TAG, "Package hit quota limit on preflight " +
                                 pkg.packageName + ": " + totalSize + " of " + mQuota);
                     }
@@ -830,7 +810,7 @@
         @Override
         public void operationComplete(long result) {
             // got the callback, and our preflightFullBackup() method is waiting for the result
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, "Preflight op complete, result=" + result);
             }
             mResult.set(result);
@@ -840,7 +820,7 @@
 
         @Override
         public void handleCancel(boolean cancelAll) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, "Preflight cancelled; failing");
             }
             mResult.set(BackupTransport.AGENT_ERROR);
@@ -997,9 +977,7 @@
 
         @Override
         public void handleCancel(boolean cancelAll) {
-            if (DEBUG) {
-                Slog.w(TAG, "Full backup cancel of " + mTarget.packageName);
-            }
+            Slog.w(TAG, "Full backup cancel of " + mTarget.packageName);
 
             mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
index 38c7dd1..87cf8a3 100644
--- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java
+++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
@@ -17,7 +17,6 @@
 package com.android.server.backup.internal;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 
 import android.app.backup.BackupAnnotations.BackupDestination;
@@ -136,8 +135,8 @@
 
     public void handleMessage(Message msg) {
         if (msg.what == MSG_STOP) {
-            Slog.v(TAG, "Stopping backup handler");
-            backupManagerService.getWakelock().quit();
+            Slog.d(TAG, "Stopping backup handler");
+            backupManagerService.getWakeLock().quit();
             mBackupThread.quitSafely();
         }
 
@@ -163,7 +162,7 @@
                         transportManager
                                 .disposeOfTransportClient(transportConnection, callerLogString);
                     }
-                    Slog.v(TAG, "Backup requested but no transport available");
+                    Slog.d(TAG, "Backup requested but no transport available");
                     break;
                 }
 
@@ -177,14 +176,12 @@
                         return;
                     }
 
-                    if (DEBUG) {
-                        Slog.v(TAG, "Running a backup pass");
-                    }
+                    Slog.d(TAG, "Running a backup pass");
 
                     // Acquire the wakelock and pass it to the backup thread. It will be released
                     // once backup concludes.
                     backupManagerService.setBackupRunning(true);
-                    backupManagerService.getWakelock().acquire();
+                    backupManagerService.getWakeLock().acquire();
 
                     // Do we have any work to do?  Construct the work queue
                     // then release the synchronization lock to actually run
@@ -193,9 +190,7 @@
                         for (BackupRequest b : backupManagerService.getPendingBackups().values()) {
                             queue.add(b.packageName);
                         }
-                        if (DEBUG) {
-                            Slog.v(TAG, "clearing pending backups");
-                        }
+                        Slog.d(TAG, "clearing pending backups");
                         backupManagerService.getPendingBackups().clear();
 
                         // Start a new backup-queue journal file too
@@ -249,7 +244,7 @@
                         staged = false;
                     }
                 } else {
-                    Slog.v(TAG, "Backup requested but nothing pending");
+                    Slog.d(TAG, "Backup requested but nothing pending");
                     staged = false;
                 }
 
@@ -259,7 +254,7 @@
                     synchronized (backupManagerService.getQueueLock()) {
                         backupManagerService.setBackupRunning(false);
                     }
-                    backupManagerService.getWakelock().release();
+                    backupManagerService.getWakeLock().release();
                 }
                 break;
             }
@@ -267,7 +262,7 @@
             case MSG_BACKUP_RESTORE_STEP: {
                 try {
                     BackupRestoreTask task = (BackupRestoreTask) msg.obj;
-                    if (MORE_DEBUG) {
+                    if (DEBUG) {
                         Slog.v(TAG, "Got next step for " + task + ", executing");
                     }
                     task.execute();
@@ -324,16 +319,12 @@
 
                 synchronized (backupManagerService.getPendingRestores()) {
                     if (backupManagerService.isRestoreInProgress()) {
-                        if (DEBUG) {
-                            Slog.d(TAG, "Restore in progress, queueing.");
-                        }
+                        Slog.d(TAG, "Restore in progress, queueing.");
                         backupManagerService.getPendingRestores().add(task);
                         // This task will be picked up and executed when the the currently running
                         // restore task finishes.
                     } else {
-                        if (DEBUG) {
-                            Slog.d(TAG, "Starting restore.");
-                        }
+                        Slog.d(TAG, "Starting restore.");
                         backupManagerService.setRestoreInProgress(true);
                         Message restoreMsg = obtainMessage(MSG_BACKUP_RESTORE_STEP, task);
                         sendMessage(restoreMsg);
@@ -471,11 +462,11 @@
 
             case MSG_REQUEST_BACKUP: {
                 BackupParams params = (BackupParams) msg.obj;
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.d(TAG, "MSG_REQUEST_BACKUP observer=" + params.observer);
                 }
                 backupManagerService.setBackupRunning(true);
-                backupManagerService.getWakelock().acquire();
+                backupManagerService.getWakeLock().acquire();
 
                 KeyValueBackupTask.start(
                         backupManagerService,
@@ -496,7 +487,7 @@
 
             case MSG_SCHEDULE_BACKUP_PACKAGE: {
                 String pkgName = (String) msg.obj;
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.d(TAG, "MSG_SCHEDULE_BACKUP_PACKAGE " + pkgName);
                 }
                 backupManagerService.dataChangedImpl(pkgName);
diff --git a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
index a94167e..0b974e2 100644
--- a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
+++ b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
@@ -17,7 +17,6 @@
 package com.android.server.backup.internal;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 
 import android.annotation.UserIdInt;
 import android.util.Slog;
@@ -206,7 +205,7 @@
      * @return true if the operation was ACKed prior to or during this call.
      */
     public boolean waitUntilOperationComplete(int token, IntConsumer callback) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, "[UserID:" + mUserId + "] Blocking until operation complete for "
                     + Integer.toHexString(token));
         }
@@ -227,7 +226,7 @@
                         }
                         // When the wait is notified we loop around and recheck the current state
                     } else {
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.d(TAG, "[UserID:" + mUserId
                                     + "] Unblocked waiting for operation token="
                                     + Integer.toHexString(token));
@@ -244,7 +243,7 @@
         if (op != null) {
             callback.accept(op.type);
         }
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(TAG, "[UserID:" + mUserId + "] operation " + Integer.toHexString(token)
                     + " complete: finalState=" + finalState);
         }
@@ -263,7 +262,7 @@
      *                 operation from PENDING to ACKNOWLEDGED state.
      */
     public void onOperationComplete(int token, long result, Consumer<BackupRestoreTask> callback) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(TAG, "[UserID:" + mUserId + "] onOperationComplete: "
                     + Integer.toHexString(token) + " result=" + result);
         }
@@ -277,10 +276,8 @@
                     op = null;
                     mOperations.remove(token);
                 } else if (op.state == OpState.ACKNOWLEDGED) {
-                    if (DEBUG) {
-                        Slog.w(TAG, "[UserID:" + mUserId + "] Received duplicate ack for token="
+                    Slog.w(TAG, "[UserID:" + mUserId + "] Received duplicate ack for token="
                                 + Integer.toHexString(token));
-                    }
                     op = null;
                     mOperations.remove(token);
                 } else if (op.state == OpState.PENDING) {
@@ -317,7 +314,7 @@
         Operation op = null;
         synchronized (mOperationsLock) {
             op = mOperations.get(token);
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 if (op == null) {
                     Slog.w(TAG, "[UserID:" + mUserId + "] Cancel of token "
                             + Integer.toHexString(token) + " but no op found");
@@ -326,17 +323,13 @@
             int state = (op != null) ? op.state : OpState.TIMEOUT;
             if (state == OpState.ACKNOWLEDGED) {
                 // The operation finished cleanly, so we have nothing more to do.
-                if (DEBUG) {
-                    Slog.w(TAG, "[UserID:" + mUserId + "] Operation already got an ack."
+                Slog.w(TAG, "[UserID:" + mUserId + "] Operation already got an ack."
                             + "Should have been removed from mCurrentOperations.");
-                }
                 op = null;
                 mOperations.delete(token);
             } else if (state == OpState.PENDING) {
-                if (DEBUG) {
-                    Slog.v(TAG, "[UserID:" + mUserId + "] Cancel: token="
+                Slog.d(TAG, "[UserID:" + mUserId + "] Cancel: token="
                             + Integer.toHexString(token));
-                }
                 op.state = OpState.TIMEOUT;
                 // Can't delete op from mOperations here. waitUntilOperationComplete may be
                 // called after we receive cancel here. We need this op's state there.
@@ -347,7 +340,7 @@
 
         // If there's a TimeoutHandler for this event, call it
         if (op != null && op.callback != null) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, "[UserID:" + mUserId + "   Invoking cancel on " + op.callback);
             }
             op.callback.handleCancel(cancelAll);
diff --git a/services/backup/java/com/android/server/backup/internal/PerformClearTask.java b/services/backup/java/com/android/server/backup/internal/PerformClearTask.java
index de0177c..b9ac564 100644
--- a/services/backup/java/com/android/server/backup/internal/PerformClearTask.java
+++ b/services/backup/java/com/android/server/backup/internal/PerformClearTask.java
@@ -75,7 +75,7 @@
             }
             mListener.onFinished(callerLogString);
             // Last but not least, release the cpu
-            mBackupManagerService.getWakelock().release();
+            mBackupManagerService.getWakeLock().release();
         }
     }
 }
diff --git a/services/backup/java/com/android/server/backup/internal/RunInitializeReceiver.java b/services/backup/java/com/android/server/backup/internal/RunInitializeReceiver.java
index 160124b..98563a7 100644
--- a/services/backup/java/com/android/server/backup/internal/RunInitializeReceiver.java
+++ b/services/backup/java/com/android/server/backup/internal/RunInitializeReceiver.java
@@ -16,7 +16,6 @@
 
 package com.android.server.backup.internal;
 
-import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.RUN_INITIALIZE_ACTION;
 
@@ -47,9 +46,7 @@
 
         synchronized (mUserBackupManagerService.getQueueLock()) {
             Set<String> pendingInits = mUserBackupManagerService.getPendingInits();
-            if (DEBUG) {
-                Slog.v(TAG, "Running a device init; " + pendingInits.size() + " pending");
-            }
+            Slog.d(TAG, "Running a device init; " + pendingInits.size() + " pending");
 
             if (pendingInits.size() > 0) {
                 String[] transports = pendingInits.toArray(new String[pendingInits.size()]);
diff --git a/services/backup/java/com/android/server/backup/internal/SetupObserver.java b/services/backup/java/com/android/server/backup/internal/SetupObserver.java
index f399fe9..309fe29 100644
--- a/services/backup/java/com/android/server/backup/internal/SetupObserver.java
+++ b/services/backup/java/com/android/server/backup/internal/SetupObserver.java
@@ -16,7 +16,7 @@
 
 package com.android.server.backup.internal;
 
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
+import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.getSetupCompleteSettingForUser;
 
@@ -57,7 +57,7 @@
 
         boolean resolvedSetupComplete = previousSetupComplete || newSetupComplete;
         mUserBackupManagerService.setSetupComplete(resolvedSetupComplete);
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.d(
                     TAG,
                     "Setup complete change: was="
@@ -73,7 +73,7 @@
             if (resolvedSetupComplete
                     && !previousSetupComplete
                     && mUserBackupManagerService.isEnabled()) {
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.d(TAG, "Setup complete so starting backups");
                 }
                 KeyValueBackupJob.schedule(mUserBackupManagerService.getUserId(), mContext,
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java
index 20c8cf6..af0c895 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java
@@ -54,13 +54,10 @@
 @VisibleForTesting
 public class KeyValueBackupReporter {
     @VisibleForTesting static final String TAG = "KeyValueBackupTask";
-    private static final boolean DEBUG = BackupManagerService.DEBUG;
-    @VisibleForTesting static final boolean MORE_DEBUG = BackupManagerService.MORE_DEBUG;
+    @VisibleForTesting static final boolean DEBUG = BackupManagerService.DEBUG;
 
     static void onNewThread(String threadName) {
-        if (DEBUG) {
-            Slog.d(TAG, "Spinning thread " + threadName);
-        }
+        Slog.d(TAG, "Spinning thread " + threadName);
     }
 
     private final UserBackupManagerService mBackupManagerService;
@@ -87,9 +84,7 @@
     }
 
     void onSkipBackup() {
-        if (DEBUG) {
-            Slog.d(TAG, "Skipping backup since one is already in progress");
-        }
+        Slog.d(TAG, "Skipping backup since one is already in progress");
     }
 
     void onEmptyQueueAtStart() {
@@ -97,9 +92,7 @@
     }
 
     void onQueueReady(List<String> queue) {
-        if (DEBUG) {
-            Slog.v(TAG, "Beginning backup of " + queue.size() + " targets");
-        }
+        Slog.d(TAG, "Beginning backup of " + queue.size() + " targets");
     }
 
     void onTransportReady(String transportName) {
@@ -167,7 +160,7 @@
     }
 
     void onAgentError(String packageName) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, "Agent failure for " + packageName + ", re-staging");
         }
         BackupObserverUtils.sendBackupOnPackageResult(
@@ -175,13 +168,11 @@
     }
 
     void onExtractAgentData(String packageName) {
-        if (DEBUG) {
-            Slog.d(TAG, "Invoking agent on " + packageName);
-        }
+        Slog.d(TAG, "Invoking agent on " + packageName);
     }
 
     void onAgentFilesReady(File backupDataFile) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.d(TAG, "Data file: " + backupDataFile);
         }
     }
@@ -216,7 +207,7 @@
                                 null, BackupManagerMonitor.EXTRA_LOG_ILLEGAL_KEY, key));
         BackupObserverUtils.sendBackupOnPackageResult(
                 mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE);
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(
                     TAG,
                     "Agent failure for " + packageName + " with illegal key " + key + ", dropped");
@@ -232,7 +223,7 @@
     }
 
     void onWriteWidgetData(boolean priorStateExists, @Nullable byte[] widgetState) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(
                     TAG,
                     "Checking widget update: state="
@@ -243,13 +234,13 @@
     }
 
     void onTransportPerformBackup(String packageName) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(TAG, "Sending non-empty data to transport for " + packageName);
         }
     }
 
     void onEmptyData(PackageInfo packageInfo) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, "No backup data written, not calling transport");
         }
         mBackupManagerMonitorEventSender.monitorEvent(
@@ -273,7 +264,7 @@
     }
 
     void onPackageBackupQuotaExceeded(String packageName) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.d(TAG, "Package " + packageName + " hit quota limit on key-value backup");
         }
         BackupObserverUtils.sendBackupOnPackageResult(
@@ -319,7 +310,7 @@
     }
 
     void onCancel() {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(TAG, "Cancel received");
         }
     }
@@ -363,7 +354,7 @@
     }
 
     void onRevertTask() {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, "Reverting backup queue, re-staging everything");
         }
     }
@@ -373,7 +364,7 @@
     }
 
     void onRemoteCallReturned(RemoteResult result, String logIdentifier) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(TAG, "Agent call " + logIdentifier + " returned " + result);
         }
     }
@@ -388,7 +379,7 @@
 
     void onTransportNotInitialized(@Nullable String transportName) {
         EventLog.writeEvent(EventLogTags.BACKUP_RESET, transportName);
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.d(TAG, "Transport requires initialization, rerunning");
         }
     }
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
index 689c43a..494b9d5 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
@@ -826,7 +826,7 @@
         }
         mTaskFinishedListener.onFinished(callerLogString);
         mReporter.onBackupFinished(getBackupFinishedStatus(mCancelled, status));
-        mBackupManagerService.getWakelock().release();
+        mBackupManagerService.getWakeLock().release();
     }
 
     private int getBackupFinishedStatus(boolean cancelled, int transportStatus) {
@@ -878,12 +878,13 @@
      * the transport or not. It's the caller responsibility to do the clean-up or delegate it.
      */
     private void extractAgentData(PackageInfo packageInfo) throws AgentException, TaskException {
-        mBackupManagerService.setWorkSource(new WorkSource(packageInfo.applicationInfo.uid));
+        mBackupManagerService.getWakeLock().setWorkSource(
+                new WorkSource(packageInfo.applicationInfo.uid));
         try {
             mAgent = bindAgent(packageInfo);
             extractAgentData(packageInfo, mAgent);
         } finally {
-            mBackupManagerService.setWorkSource(null);
+            mBackupManagerService.getWakeLock().setWorkSource(null);
         }
     }
 
diff --git a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
index 9f7b627..41134d6 100644
--- a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
+++ b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
@@ -17,7 +17,6 @@
 package com.android.server.backup.restore;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT;
 import static com.android.server.backup.internal.BackupHandler.MSG_RUN_GET_RESTORE_SETS;
 import static com.android.server.backup.internal.BackupHandler.MSG_RUN_RESTORE;
@@ -41,6 +40,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalServices;
+import com.android.server.backup.BackupWakeLock;
 import com.android.server.backup.Flags;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
@@ -121,7 +121,7 @@
             // comes in.
             mBackupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
 
-            UserBackupManagerService.BackupWakeLock wakelock = mBackupManagerService.getWakelock();
+            BackupWakeLock wakelock = mBackupManagerService.getWakeLock();
             wakelock.acquire();
 
             // Prevent lambda from leaking 'this'
@@ -150,10 +150,8 @@
                 android.Manifest.permission.BACKUP,
                 "performRestore");
 
-        if (DEBUG) {
-            Slog.d(TAG, "restoreAll token=" + Long.toHexString(token)
+        Slog.d(TAG, "restoreAll token=" + Long.toHexString(token)
                     + " observer=" + observer);
-        }
 
         if (mEnded) {
             throw new IllegalStateException("Restore session already ended");
@@ -213,40 +211,38 @@
                 android.Manifest.permission.BACKUP,
                 "performRestore");
 
-        if (DEBUG) {
-            StringBuilder b = new StringBuilder(128);
-            b.append("restorePackages token=");
-            b.append(Long.toHexString(token));
-            b.append(" observer=");
-            if (observer == null) {
-                b.append("null");
-            } else {
-                b.append(observer.toString());
-            }
-            b.append(" monitor=");
-            if (monitor == null) {
-                b.append("null");
-            } else {
-                b.append(monitor.toString());
-            }
-            b.append(" packages=");
-            if (packages == null) {
-                b.append("null");
-            } else {
-                b.append('{');
-                boolean first = true;
-                for (String s : packages) {
-                    if (!first) {
-                        b.append(", ");
-                    } else {
-                        first = false;
-                    }
-                    b.append(s);
-                }
-                b.append('}');
-            }
-            Slog.d(TAG, b.toString());
+        StringBuilder b = new StringBuilder(128);
+        b.append("restorePackages token=");
+        b.append(Long.toHexString(token));
+        b.append(" observer=");
+        if (observer == null) {
+            b.append("null");
+        } else {
+            b.append(observer.toString());
         }
+        b.append(" monitor=");
+        if (monitor == null) {
+            b.append("null");
+        } else {
+            b.append(monitor.toString());
+        }
+        b.append(" packages=");
+        if (packages == null) {
+            b.append("null");
+        } else {
+            b.append('{');
+            boolean first = true;
+            for (String s : packages) {
+                if (!first) {
+                    b.append(", ");
+                } else {
+                    first = false;
+                }
+                b.append(s);
+            }
+            b.append('}');
+        }
+        Slog.d(TAG, b.toString());
 
         if (mEnded) {
             throw new IllegalStateException("Restore session already ended");
@@ -325,10 +321,8 @@
 
     public synchronized int restorePackage(String packageName, IRestoreObserver observer,
             IBackupManagerMonitor monitor) {
-        if (DEBUG) {
-            Slog.v(TAG, "restorePackage pkg=" + packageName + " obs=" + observer
+        Slog.d(TAG, "restorePackage pkg=" + packageName + " obs=" + observer
                     + "monitor=" + monitor);
-        }
 
         if (mEnded) {
             throw new IllegalStateException("Restore session already ended");
@@ -379,18 +373,14 @@
             // Check whether there is data for it in the current dataset, falling back
             // to the ancestral dataset if not.
             long token = mBackupManagerService.getAvailableRestoreToken(packageName);
-            if (DEBUG) {
-                Slog.v(TAG, "restorePackage pkg=" + packageName
+            Slog.d(TAG, "restorePackage pkg=" + packageName
                         + " token=" + Long.toHexString(token));
-            }
 
             // If we didn't come up with a place to look -- no ancestral dataset and
             // the app has never been backed up from this device -- there's nothing
             // to do but return failure.
             if (token == 0) {
-                if (DEBUG) {
-                    Slog.w(TAG, "No data available for this package; not restoring");
-                }
+                Slog.w(TAG, "No data available for this package; not restoring");
                 return -1;
             }
 
@@ -431,9 +421,9 @@
         Handler backupHandler = mBackupManagerService.getBackupHandler();
         backupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
 
-        UserBackupManagerService.BackupWakeLock wakelock = mBackupManagerService.getWakelock();
+        BackupWakeLock wakelock = mBackupManagerService.getWakeLock();
         wakelock.acquire();
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.d(TAG, callerLogString);
         }
 
@@ -473,9 +463,7 @@
     }
 
     public synchronized void endRestoreSession() {
-        if (DEBUG) {
-            Slog.d(TAG, "endRestoreSession");
-        }
+        Slog.d(TAG, "endRestoreSession");
 
         if (mTimedOut) {
             Slog.i(TAG, "Session already timed out");
diff --git a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
index cfc0f20..cb491c6 100644
--- a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
+++ b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
@@ -17,7 +17,6 @@
 package com.android.server.backup.restore;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 
 import android.util.Slog;
 
@@ -72,7 +71,7 @@
 
     @Override
     public void operationComplete(long result) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.w(TAG, "adb onRestoreFinished() complete");
         }
         mLatch.countDown();
@@ -81,9 +80,7 @@
 
     @Override
     public void handleCancel(boolean cancelAll) {
-        if (DEBUG) {
-            Slog.w(TAG, "adb onRestoreFinished() timed out");
-        }
+        Slog.w(TAG, "adb onRestoreFinished() timed out");
         mLatch.countDown();
         mOperationStorage.removeOperation(mCurrentOpToken);
     }
diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
index 237ffa3..19e565d 100644
--- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
+++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
@@ -17,7 +17,6 @@
 package com.android.server.backup.restore;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
@@ -215,12 +214,12 @@
 
         FileMetadata info;
         try {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, "Reading tar header for restoring file");
             }
             info = tarBackupReader.readTarHeaders();
             if (info != null) {
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     info.dump();
                 }
 
@@ -249,9 +248,7 @@
                     // Clean up the previous agent relationship if necessary,
                     // and let the observer know we're considering a new app.
                     if (mAgent != null) {
-                        if (DEBUG) {
-                            Slog.d(TAG, "Saw new package; finalizing old one");
-                        }
+                        Slog.d(TAG, "Saw new package; finalizing old one");
                         // Now we're really done
                         tearDownPipes();
                         tearDownAgent(mTargetApp, mIsAdbRestore);
@@ -307,9 +304,7 @@
                             // If we're in accept-if-apk state, then the first file we
                             // see MUST be the apk.
                             if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) {
-                                if (DEBUG) {
-                                    Slog.d(TAG, "APK file; installing");
-                                }
+                                Slog.d(TAG, "APK file; installing");
                                 // Try to install the app.
                                 String installerPackageName = mPackageInstallers.get(pkg);
                                 boolean isSuccessfullyInstalled = RestoreUtils.installApk(
@@ -336,9 +331,7 @@
 
                         case ACCEPT:
                             if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) {
-                                if (DEBUG) {
-                                    Slog.d(TAG, "apk present but ACCEPT");
-                                }
+                                Slog.d(TAG, "apk present but ACCEPT");
                                 // we can take the data without the apk, so we
                                 // *want* to do so.  skip the apk by declaring this
                                 // one file not-okay without changing the restore
@@ -364,11 +357,11 @@
 
                     // If the policy is satisfied, go ahead and set up to pipe the
                     // data to the agent.
-                    if (MORE_DEBUG && okay && mAgent != null) {
+                    if (DEBUG && okay && mAgent != null) {
                         Slog.i(TAG, "Reusing existing agent instance");
                     }
                     if (okay && mAgent == null) {
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.d(TAG, "Need to launch agent for " + pkg);
                         }
 
@@ -389,20 +382,18 @@
                                 boolean forceClear = shouldForceClearAppDataOnFullRestore(
                                         mTargetApp.packageName);
                                 if (mTargetApp.backupAgentName == null || forceClear) {
-                                    if (DEBUG) {
-                                        Slog.d(TAG,
+                                    Slog.d(TAG,
                                                 "Clearing app data preparatory to full restore");
-                                    }
                                     mBackupManagerService.clearApplicationDataBeforeRestore(pkg);
                                 } else {
-                                    if (MORE_DEBUG) {
+                                    if (DEBUG) {
                                         Slog.d(TAG, "backup agent ("
                                                 + mTargetApp.backupAgentName + ") => no clear");
                                     }
                                 }
                                 mClearedPackages.add(pkg);
                             } else {
-                                if (MORE_DEBUG) {
+                                if (DEBUG) {
                                     Slog.d(TAG, "We've initialized this app already; no clear "
                                             + "required");
                                 }
@@ -459,10 +450,8 @@
                                     OpType.RESTORE_WAIT);
 
                             if (FullBackup.OBB_TREE_TOKEN.equals(info.domain)) {
-                                if (DEBUG) {
-                                    Slog.d(TAG, "Restoring OBB file for " + pkg
+                                Slog.d(TAG, "Restoring OBB file for " + pkg
                                             + " : " + info.path);
-                                }
                                 mObbConnection.restoreObbFile(pkg, mPipes[0],
                                         info.size, info.type, info.path, info.mode,
                                         info.mtime, token,
@@ -470,10 +459,8 @@
                             } else if (FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)) {
                                 // This is only possible during adb restore.
                                 // TODO: Refactor to clearly separate the flows.
-                                if (DEBUG) {
-                                    Slog.d(TAG, "Restoring key-value file for " + pkg
+                                Slog.d(TAG, "Restoring key-value file for " + pkg
                                             + " : " + info.path);
-                                }
                                 // Set the version saved from manifest entry.
                                 info.version = mAppVersion;
                                 KeyValueAdbRestoreEngine restoreEngine =
@@ -483,7 +470,7 @@
                                                 mAgent, token);
                                 new Thread(restoreEngine, "restore-key-value-runner").start();
                             } else {
-                                if (MORE_DEBUG) {
+                                if (DEBUG) {
                                     Slog.d(TAG, "Invoking agent to restore file " + info.path);
                                 }
                                 // fire up the app's agent listening on the socket.  If
@@ -519,7 +506,7 @@
 
                         // Copy over the data if the agent is still good
                         if (okay) {
-                            if (MORE_DEBUG) {
+                            if (DEBUG) {
                                 Slog.v(TAG, "  copying to restore agent: " + toCopy + " bytes");
                             }
                             boolean pipeOkay = true;
@@ -586,7 +573,7 @@
                     // dropped file, or an already-ignored package: skip to the
                     // next stream entry by reading and discarding this file.
                     if (!okay) {
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.d(TAG, "[discarding file content]");
                         }
                         long bytesToConsume = (info.size + 511) & ~511;
@@ -603,9 +590,7 @@
                 }
             }
         } catch (IOException e) {
-            if (DEBUG) {
-                Slog.w(TAG, "io exception on restore socket read: " + e.getMessage());
-            }
+            Slog.w(TAG, "io exception on restore socket read: " + e.getMessage());
             logBMMEvent(BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT,
                     onlyPackage);
             setResult(RestoreEngine.TRANSPORT_FAILURE);
@@ -614,7 +599,7 @@
 
         // If we got here we're either running smoothly or we've finished
         if (info == null) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, "No [more] data for this package; tearing down");
             }
             tearDownPipes();
@@ -713,7 +698,7 @@
                     mBackupManagerService.prepareOperationTimeout(
                             token, fullBackupAgentTimeoutMillis, latch, OpType.RESTORE_WAIT);
                     if (mTargetApp.processName.equals("system")) {
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.d(TAG, "system agent - restoreFinished on thread");
                         }
                         Runnable runner = new AdbRestoreFinishedRunnable(mAgent, token,
@@ -749,7 +734,7 @@
             return true;
         }
         if (FullBackup.CACHE_TREE_TOKEN.equals(info.domain)) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, "Dropping cache file path " + info.path);
             }
             return false;
@@ -761,7 +746,7 @@
             // API.  Respect the no-backup intention and don't let the data get to
             // the app.
             if (info.path.startsWith("no_backup/")) {
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.i(TAG, "Dropping no_backup file path " + info.path);
                 }
                 return false;
@@ -774,7 +759,7 @@
 
     private static boolean isCanonicalFilePath(String path) {
         if (path.contains("..") || path.contains("//")) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.w(TAG, "Dropping invalid path " + path);
             }
             return false;
diff --git a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
index 2374dee..5a3494c 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -17,7 +17,6 @@
 package com.android.server.backup.restore;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT;
 import static com.android.server.backup.BackupPasswordManager.PBKDF_FALLBACK;
@@ -93,9 +92,7 @@
         FileInputStream rawInStream = null;
         try {
             if (!mBackupManagerService.backupPasswordMatches(mCurrentPassword)) {
-                if (DEBUG) {
-                    Slog.w(TAG, "Backup password mismatch; aborting");
-                }
+                Slog.w(TAG, "Backup password mismatch; aborting");
                 return;
             }
 
@@ -122,7 +119,7 @@
                     tarInputStream);
             mEngineThread.run();
 
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, "Done consuming input tarfile.");
             }
         } catch (Exception e) {
@@ -144,7 +141,7 @@
             mObbConnection.tearDown();
             mObserver = FullBackupRestoreObserverUtils.sendEndRestore(mObserver);
             Slog.d(TAG, "Full restore pass complete.");
-            mBackupManagerService.getWakelock().release();
+            mBackupManagerService.getWakeLock().release();
         }
     }
 
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index ec9d340..707ae03 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -19,7 +19,6 @@
 import static android.app.backup.BackupAnnotations.OperationType.RESTORE;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.KEY_WIDGET_STATE;
 import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
@@ -253,9 +252,7 @@
                                 mUserId,
                                 backupEligibilityRules);
                 filterSet = packagesToNames(apps);
-                if (DEBUG) {
-                    Slog.i(TAG, "Full restore; asking about " + filterSet.length + " apps");
-                }
+                Slog.i(TAG, "Full restore; asking about " + filterSet.length + " apps");
             }
 
             mAcceptSet = new ArrayList<>(filterSet.length);
@@ -315,7 +312,7 @@
             }
         }
 
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(TAG, "Restore; accept set size is " + mAcceptSet.size());
             for (PackageInfo info : mAcceptSet) {
                 Slog.v(TAG, "   " + info.packageName);
@@ -335,7 +332,7 @@
     // Execute one tick of whatever state machine the task implements
     @Override
     public void execute() {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.v(TAG, "*** Executing restore step " + mState);
         }
         switch (mState) {
@@ -517,7 +514,7 @@
             mCurrentPackage.applicationInfo.uid = Process.SYSTEM_UID;
             mPmAgent = backupManagerService.makeMetadataAgent(null);
             mAgent = IBackupAgent.Stub.asInterface(mPmAgent.onBind());
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, "initiating restore for PMBA");
             }
             initiateOneRestore(mCurrentPackage, 0);
@@ -596,9 +593,7 @@
                 return;
             } else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) {
                 // Yay we've reached the end cleanly
-                if (DEBUG) {
-                    Slog.v(TAG, "No more packages; finishing restore");
-                }
+                Slog.d(TAG, "No more packages; finishing restore");
                 int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime);
                 EventLog.writeEvent(
                         EventLogTags.RESTORE_SUCCESS, mRestoreAttemptedAppsCount, millis);
@@ -606,9 +601,7 @@
                 return;
             }
 
-            if (DEBUG) {
-                Slog.i(TAG, "Next restore package: " + mRestoreDescription);
-            }
+            Slog.i(TAG, "Next restore package: " + mRestoreDescription);
             mRestoreAttemptedAppsCount++;
             sendOnRestorePackage(mRestoreAttemptedAppsCount, pkgName);
 
@@ -715,7 +708,7 @@
                 }
             }
 
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(
                         TAG,
                         "Package "
@@ -776,7 +769,7 @@
                 /* extras= */ addRestoreOperationTypeToEvent(/* extras= */ null));
         if (mCurrentPackage.applicationInfo.backupAgentName == null
                 || "".equals(mCurrentPackage.applicationInfo.backupAgentName)) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(
                         TAG,
                         "Data exists for package "
@@ -852,9 +845,7 @@
     private void initiateOneRestore(PackageInfo app, long appVersionCode) {
         final String packageName = app.packageName;
 
-        if (DEBUG) {
-            Slog.d(TAG, "initiateOneRestore packageName=" + packageName);
-        }
+        Slog.d(TAG, "initiateOneRestore packageName=" + packageName);
 
         // !!! TODO: get the dirs from the transport
         mBackupDataName = new File(backupManagerService.getDataDir(), packageName + ".restore");
@@ -1010,9 +1001,7 @@
 
             // is this a special key?
             if (key.equals(KEY_WIDGET_STATE)) {
-                if (DEBUG) {
-                    Slog.i(TAG, "Restoring widget state for " + packageName);
-                }
+                Slog.i(TAG, "Restoring widget state for " + packageName);
                 mWidgetData = new byte[size];
                 in.readEntityData(mWidgetData, 0, size);
             } else {
@@ -1044,7 +1033,7 @@
                 /* extras= */ addRestoreOperationTypeToEvent(/* extras= */ null));
         try {
             StreamFeederThread feeder = new StreamFeederThread();
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(
                         TAG,
                         "Spinning threads for stream restore of " + mCurrentPackage.packageName);
@@ -1070,9 +1059,7 @@
 
     // state RESTORE_FINISHED : provide the "no more data" signpost callback at the end
     private void restoreFinished() {
-        if (DEBUG) {
-            Slog.d(TAG, "restoreFinished packageName=" + mCurrentPackage.packageName);
-        }
+        Slog.d(TAG, "restoreFinished packageName=" + mCurrentPackage.packageName);
         try {
             long restoreAgentFinishedTimeoutMillis =
                     mAgentTimeoutParameters.getRestoreAgentFinishedTimeoutMillis();
@@ -1167,7 +1154,7 @@
                     if (result > 0) {
                         // The transport wrote this many bytes of restore data to the
                         // pipe, so pass it along to the engine.
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.v(TAG, "  <- transport provided chunk size " + result);
                         }
                         if (result > bufferSize) {
@@ -1179,13 +1166,13 @@
                             int n = transportIn.read(buffer, 0, toCopy);
                             engineOut.write(buffer, 0, n);
                             toCopy -= n;
-                            if (MORE_DEBUG) {
+                            if (DEBUG) {
                                 Slog.v(TAG, "  -> wrote " + n + " to engine, left=" + toCopy);
                             }
                         }
                     } else if (result == BackupTransport.NO_MORE_DATA) {
                         // Clean finish.  Wind up and we're done!
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.i(
                                     TAG,
                                     "Got clean full-restore EOF for "
@@ -1213,7 +1200,7 @@
                         status = result;
                     }
                 }
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.v(TAG, "Done copying to engine, falling through");
                 }
             } catch (IOException e) {
@@ -1322,9 +1309,7 @@
         @Override
         public void handleCancel(boolean cancelAll) {
             mOperationStorage.removeOperation(mEphemeralOpToken);
-            if (DEBUG) {
-                Slog.w(TAG, "Full-data restore target timed out; shutting down");
-            }
+            Slog.w(TAG, "Full-data restore target timed out; shutting down");
             Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null);
             mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT,
@@ -1342,7 +1327,7 @@
 
     // state FINAL : tear everything down and we're done.
     private void finalizeRestore() {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.d(TAG, "finishing restore mObserver=" + mObserver);
         }
 
@@ -1366,7 +1351,7 @@
         // If we have a PM token, we must under all circumstances be sure to
         // handshake when we've finished.
         if (mPmToken > 0) {
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, "finishing PM token " + mPmToken);
             }
             try {
@@ -1404,9 +1389,7 @@
 
         synchronized (backupManagerService.getPendingRestores()) {
             if (backupManagerService.getPendingRestores().size() > 0) {
-                if (DEBUG) {
-                    Slog.d(TAG, "Starting next pending restore.");
-                }
+                Slog.d(TAG, "Starting next pending restore.");
                 PerformUnifiedRestoreTask task = backupManagerService.getPendingRestores().remove();
                 backupManagerService
                         .getBackupHandler()
@@ -1417,7 +1400,7 @@
 
             } else {
                 backupManagerService.setRestoreInProgress(false);
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.d(TAG, "No pending restores.");
                 }
             }
@@ -1499,7 +1482,7 @@
     public void operationComplete(long unusedResult) {
         mOperationStorage.removeOperation(mEphemeralOpToken);
 
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(
                     TAG,
                     "operationComplete() during restore: target="
@@ -1590,7 +1573,7 @@
 
     @VisibleForTesting
     void executeNextState(UnifiedRestoreState nextState) {
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, " => executing next step on " + this + " nextState=" + nextState);
         }
         mState = nextState;
@@ -1689,25 +1672,19 @@
         //      (and not in the denylist)
         //    - The package has restoreAnyVersion set to true and is not part of the denylist
         if (mVToUDenylist.contains(mCurrentPackage.packageName)){
-            if (DEBUG) {
-                Slog.i(TAG, mCurrentPackage.packageName + " : Package is in V to U denylist");
-            }
+            Slog.i(TAG, mCurrentPackage.packageName + " : Package is in V to U denylist");
             return false;
         } else if ((mCurrentPackage.applicationInfo.flags
                 & ApplicationInfo.FLAG_RESTORE_ANY_VERSION)
                 == 0) {
             // package has restoreAnyVersion set to false
-            if (DEBUG) {
-                Slog.i(TAG, mCurrentPackage.packageName
+            Slog.i(TAG, mCurrentPackage.packageName
                         + " : Package has restoreAnyVersion=false and is in V to U allowlist");
-            }
             return mVToUAllowlist.contains(mCurrentPackage.packageName);
         } else {
             // package has restoreAnyVersion set to true and is nor in denylist
-            if (DEBUG) {
-                Slog.i(TAG, mCurrentPackage.packageName
+            Slog.i(TAG, mCurrentPackage.packageName
                         + " : Package has restoreAnyVersion=true and is not in V to U denylist");
-            }
             return true;
         }
     }
diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
index 508b62c..3024301 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
@@ -16,7 +16,6 @@
 
 package com.android.server.backup.utils;
 
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
 import static com.android.server.backup.UserBackupManagerService.SETTINGS_PACKAGE;
@@ -46,6 +45,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
+import com.android.server.backup.BackupManagerService;
 import com.android.server.backup.SetUtils;
 import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
@@ -413,8 +413,8 @@
         // partition will be signed with the device's platform certificate, so on
         // different phones the same system app will have different signatures.)
         if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
-            if (MORE_DEBUG) {
-                Slog.v(TAG, "System app " + target.packageName + " - skipping sig check");
+            if (BackupManagerService.DEBUG) {
+                Slog.d(TAG, "System app " + target.packageName + " - skipping sig check");
             }
             return true;
         }
@@ -431,10 +431,8 @@
             return false;
         }
 
-        if (DEBUG) {
-            Slog.v(TAG, "signaturesMatch(): stored=" + Arrays.toString(storedSigs)
+        Slog.d(TAG, "signaturesMatch(): stored=" + Arrays.toString(storedSigs)
                     + " device=" + Arrays.toString(signingInfo.getApkContentsSigners()));
-        }
 
         final int nStored = storedSigs.length;
         if (nStored == 1) {
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
index 549d08c0..c4519b1 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
@@ -22,7 +22,6 @@
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT;
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING_RESULTS;
 
-import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 
 import android.annotation.Nullable;
@@ -115,15 +114,11 @@
             if (mMonitor != null) {
                 mMonitor.onEvent(bundle);
             } else {
-                if (DEBUG) {
-                    Slog.w(TAG, "backup manager monitor is null unable to send event");
-                }
+                Slog.w(TAG, "backup manager monitor is null unable to send event");
             }
         } catch (RemoteException e) {
             mMonitor = null;
-            if (DEBUG) {
-                Slog.w(TAG, "backup manager monitor went away");
-            }
+            Slog.w(TAG, "backup manager monitor went away");
         }
     }
 
diff --git a/services/backup/java/com/android/server/backup/utils/BackupObserverUtils.java b/services/backup/java/com/android/server/backup/utils/BackupObserverUtils.java
index c0cf2ef..95e6cfc 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupObserverUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupObserverUtils.java
@@ -16,7 +16,6 @@
 
 package com.android.server.backup.utils;
 
-import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 
 import android.app.backup.BackupProgress;
@@ -38,9 +37,7 @@
             try {
                 observer.onUpdate(packageName, progress);
             } catch (RemoteException e) {
-                if (DEBUG) {
-                    Slog.w(TAG, "Backup observer went away: onUpdate");
-                }
+                Slog.w(TAG, "Backup observer went away: onUpdate");
             }
         }
     }
@@ -55,9 +52,7 @@
             try {
                 observer.onResult(packageName, status);
             } catch (RemoteException e) {
-                if (DEBUG) {
-                    Slog.w(TAG, "Backup observer went away: onResult");
-                }
+                Slog.w(TAG, "Backup observer went away: onResult");
             }
         }
     }
@@ -71,9 +66,7 @@
             try {
                 observer.backupFinished(status);
             } catch (RemoteException e) {
-                if (DEBUG) {
-                    Slog.w(TAG, "Backup observer went away: backupFinished");
-                }
+                Slog.w(TAG, "Backup observer went away: backupFinished");
             }
         }
     }
diff --git a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
index 5a8533a..23e5bb9 100644
--- a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java
@@ -16,7 +16,6 @@
 
 package com.android.server.backup.utils;
 
-import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 
 import android.content.Context;
@@ -78,9 +77,7 @@
             int userId) {
         boolean okay = true;
 
-        if (DEBUG) {
-            Slog.d(TAG, "Installing from backup: " + info.packageName);
-        }
+        Slog.d(TAG, "Installing from backup: " + info.packageName);
 
         try {
             LocalIntentReceiver receiver = new LocalIntentReceiver();
diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
index 22eefb3..44b536a 100644
--- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
+++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
@@ -36,7 +36,6 @@
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER;
 
 import static com.android.server.backup.BackupManagerService.DEBUG;
-import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_VERSION;
@@ -175,7 +174,7 @@
                     }
                     case 0: {
                         // presume EOF
-                        if (MORE_DEBUG) {
+                        if (DEBUG) {
                             Slog.w(TAG, "Saw type=0 in tar header block, info=" + info);
                         }
                         return null;
@@ -195,9 +194,7 @@
                     info.path = info.path.substring(FullBackup.SHARED_PREFIX.length());
                     info.packageName = SHARED_BACKUP_AGENT_PACKAGE;
                     info.domain = FullBackup.SHARED_STORAGE_TOKEN;
-                    if (DEBUG) {
-                        Slog.i(TAG, "File in shared storage: " + info.path);
-                    }
+                    Slog.i(TAG, "File in shared storage: " + info.path);
                 } else if (FullBackup.APPS_PREFIX.regionMatches(0,
                         info.path, 0, FullBackup.APPS_PREFIX.length())) {
                     // App content!  Parse out the package name and domain
@@ -227,11 +224,9 @@
                     }
                 }
             } catch (IOException e) {
+                Slog.e(TAG, "Parse error in header: " + e.getMessage());
                 if (DEBUG) {
-                    Slog.e(TAG, "Parse error in header: " + e.getMessage());
-                    if (MORE_DEBUG) {
-                        hexLog(block);
-                    }
+                    hexLog(block);
                 }
                 throw e;
             }
@@ -254,20 +249,20 @@
         if (size <= 0) {
             throw new IllegalArgumentException("size must be > 0");
         }
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG, "  ... readExactly(" + size + ") called");
         }
         int soFar = 0;
         while (soFar < size) {
             int nRead = in.read(buffer, offset + soFar, size - soFar);
             if (nRead <= 0) {
-                if (MORE_DEBUG) {
+                if (DEBUG) {
                     Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar);
                 }
                 break;
             }
             soFar += nRead;
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.v(TAG, "   + got " + nRead + "; now wanting " + (size - soFar));
             }
         }
@@ -290,7 +285,7 @@
         }
 
         byte[] buffer = new byte[(int) info.size];
-        if (MORE_DEBUG) {
+        if (DEBUG) {
             Slog.i(TAG,
                     "   readAppManifestAndReturnSignatures() looking for " + info.size + " bytes");
         }
@@ -514,10 +509,7 @@
                             null);
                 }
             } else {
-                if (DEBUG) {
-                    Slog.i(TAG,
-                            "Restore manifest from " + info.packageName + " but allowBackup=false");
-                }
+                Slog.i(TAG, "Restore manifest from " + info.packageName + " but allowBackup=false");
                 mBackupManagerMonitorEventSender.monitorEvent(
                         LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE,
                         pkgInfo,
@@ -529,10 +521,8 @@
             // the restore properly only if the dataset provides the
             // apk file and we can successfully install it.
             if (allowApks) {
-                if (DEBUG) {
-                    Slog.i(TAG, "Package " + info.packageName
-                            + " not installed; requiring apk in dataset");
-                }
+                Slog.i(TAG,
+                        "Package " + info.packageName + " not installed; requiring apk in dataset");
                 policy = RestorePolicy.ACCEPT_IF_APK;
             } else {
                 policy = RestorePolicy.IGNORE;
@@ -570,7 +560,7 @@
         long partial = (size + 512) % 512;
         if (partial > 0) {
             final int needed = 512 - (int) partial;
-            if (MORE_DEBUG) {
+            if (DEBUG) {
                 Slog.i(TAG, "Skipping tar padding: " + needed + " bytes");
             }
             byte[] buffer = new byte[needed];
@@ -619,7 +609,7 @@
                     }
                     switch (token) {
                         case BACKUP_WIDGET_METADATA_TOKEN: {
-                            if (MORE_DEBUG) {
+                            if (DEBUG) {
                                 Slog.i(TAG, "Got widget metadata for " + info.packageName);
                             }
                             mWidgetData = new byte[size];
@@ -627,10 +617,9 @@
                             break;
                         }
                         default: {
-                            if (DEBUG) {
-                                Slog.i(TAG, "Ignoring metadata blob " + Integer.toHexString(token)
-                                        + " for " + info.packageName);
-                            }
+                            Slog.i(TAG,
+                                    "Ignoring metadata blob " + Integer.toHexString(token) + " for "
+                                            + info.packageName);
                             in.skipBytes(size);
                             break;
                         }
@@ -759,9 +748,7 @@
             } else if ("size".equals(keyStr)) {
                 info.size = Long.parseLong(valStr);
             } else {
-                if (DEBUG) {
-                    Slog.i(TAG, "Unhandled pax key: " + key);
-                }
+                Slog.i(TAG, "Unhandled pax key: " + key);
             }
 
             offset += linelen;
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
index 4b3981c..30c2a82 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
@@ -43,6 +43,8 @@
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -176,6 +178,8 @@
         writeAndClearOldAccessHistory();
         boolean assembleChains = attributionExemptPkgs != null;
         IntArray opCodes = getAppOpCodes(filter, opNamesFilter);
+        beginTimeMillis = Math.max(beginTimeMillis, Instant.now().minus(sDiscreteHistoryCutoff,
+                ChronoUnit.MILLIS).toEpochMilli());
         List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
                 packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis,
                 endTimeMillis, -1, null);
diff --git a/services/core/java/com/android/server/media/MediaSessionDeviceConfig.java b/services/core/java/com/android/server/media/MediaSessionDeviceConfig.java
index 7e747ce..2c6cebf 100644
--- a/services/core/java/com/android/server/media/MediaSessionDeviceConfig.java
+++ b/services/core/java/com/android/server/media/MediaSessionDeviceConfig.java
@@ -46,6 +46,7 @@
     private static volatile long sMediaSessionCallbackFgsAllowlistDurationMs =
             DEFAULT_MEDIA_SESSION_CALLBACK_FGS_ALLOWLIST_DURATION_MS;
 
+
     /**
      * Denotes the duration for which an app receiving a media session callback and the FGS started
      * there can be temporarily allowed to have while-in-use permissions such as
@@ -58,6 +59,16 @@
     private static volatile long sMediaSessionCallbackFgsWhileInUseTempAllowDurationMs =
             DEFAULT_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS;
 
+    /**
+     * Denotes the duration (in milliseconds) that a media session can remain in an engaged state,
+     * where it is only considered engaged if transitioning from active playback.
+     */
+    private static final String KEY_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS =
+            "media_session_temp_user_engaged_duration_ms";
+    private static final long DEFAULT_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS = 600_000;
+    private static volatile long sMediaSessionTempUserEngagedDurationMs =
+            DEFAULT_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS;
+
     private static void refresh(DeviceConfig.Properties properties) {
         final Set<String> keys = properties.getKeyset();
         properties.getKeyset().forEach(key -> {
@@ -73,6 +84,11 @@
                 case KEY_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS:
                     sMediaSessionCallbackFgsWhileInUseTempAllowDurationMs = properties.getLong(key,
                             DEFAULT_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS);
+                    break;
+                case KEY_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS:
+                    sMediaSessionTempUserEngagedDurationMs = properties.getLong(key,
+                            DEFAULT_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS);
+                    break;
             }
         });
     }
@@ -110,6 +126,15 @@
         return sMediaSessionCallbackFgsWhileInUseTempAllowDurationMs;
     }
 
+    /**
+     * Returns the duration (in milliseconds) that a media session can remain in an engaged state,
+     * where it is only considered engaged if transitioning from active playback. After this
+     * duration, the session is disengaged until explicit user action triggers active playback.
+     */
+    public static long getMediaSessionTempUserEngagedDurationMs() {
+        return sMediaSessionTempUserEngagedDurationMs;
+    }
+
     public static void dump(PrintWriter pw, String prefix) {
         pw.println("Media session config:");
         final String dumpFormat = prefix + "  %s: [cur: %s, def: %s]";
@@ -125,5 +150,9 @@
                 KEY_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS,
                 sMediaSessionCallbackFgsWhileInUseTempAllowDurationMs,
                 DEFAULT_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS));
+        pw.println(TextUtils.formatSimple(dumpFormat,
+                KEY_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS,
+                sMediaSessionTempUserEngagedDurationMs,
+                DEFAULT_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS));
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 5f7c86f..d873075 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -264,15 +264,6 @@
      */
     private static final int USER_DISENGAGED = 2;
 
-    /**
-     * Indicates the duration of the temporary engaged state, in milliseconds.
-     *
-     * <p>When switching to an {@linkplain PlaybackState#isActive() inactive state}, the user is
-     * treated as temporarily engaged, meaning the corresponding session is only considered in an
-     * engaged state for the duration of this timeout, and only if coming from an engaged state.
-     */
-    private static final int TEMP_USER_ENGAGED_TIMEOUT_MS = 600000;
-
     public MediaSessionRecord(
             int ownerPid,
             int ownerUid,
@@ -605,7 +596,8 @@
                             if (mUserEngagementState == USER_TEMPORARILY_ENGAGED) {
                                 mHandler.postDelayed(
                                         mUserEngagementTimeoutExpirationRunnable,
-                                        TEMP_USER_ENGAGED_TIMEOUT_MS);
+                                        MediaSessionDeviceConfig
+                                                .getMediaSessionTempUserEngagedDurationMs());
                             }
                         }
                     }
@@ -1079,7 +1071,8 @@
         mUserEngagementState = newUserEngagedState;
         if (newUserEngagedState == USER_TEMPORARILY_ENGAGED && !isGlobalPrioritySessionActive) {
             mHandler.postDelayed(
-                    mUserEngagementTimeoutExpirationRunnable, TEMP_USER_ENGAGED_TIMEOUT_MS);
+                    mUserEngagementTimeoutExpirationRunnable,
+                    MediaSessionDeviceConfig.getMediaSessionTempUserEngagedDurationMs());
         } else {
             mHandler.removeCallbacks(mUserEngagementTimeoutExpirationRunnable);
         }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index a0fbc00..a8f31f9 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -849,7 +849,9 @@
         int token;
         if (mPm.mNextInstallToken < 0) mPm.mNextInstallToken = 1;
         token = mPm.mNextInstallToken++;
-        mPm.mRunningInstalls.put(token, request);
+        synchronized (mPm.mRunningInstalls) {
+            mPm.mRunningInstalls.put(token, request);
+        }
 
         if (DEBUG_INSTALL) Log.v(TAG, "+ starting restore round-trip " + token);
 
diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java
index bc03b10b..9916be6 100644
--- a/services/core/java/com/android/server/pm/PackageHandler.java
+++ b/services/core/java/com/android/server/pm/PackageHandler.java
@@ -82,14 +82,20 @@
             case POST_INSTALL: {
                 if (DEBUG_INSTALL) Log.v(TAG, "Handling post-install for " + msg.arg1);
 
-                InstallRequest request = mPm.mRunningInstalls.get(msg.arg1);
-                final boolean didRestore = (msg.arg2 != 0);
-                mPm.mRunningInstalls.delete(msg.arg1);
+                final InstallRequest request;
+                final int token;
+                final boolean didRestore;
+                synchronized (mPm.mRunningInstalls) {
+                    request= mPm.mRunningInstalls.get(msg.arg1);
+                    token = msg.arg1;
+                    didRestore = (msg.arg2 != 0);
+                    mPm.mRunningInstalls.delete(token);
+                }
 
                 if (request == null) {
                     if (DEBUG_INSTALL) {
                         Slog.i(TAG, "InstallRequest is null. Nothing to do for post-install "
-                                + "token " + msg.arg1);
+                                + "token " + token);
                     }
                     break;
                 }
@@ -100,10 +106,10 @@
                     mPm.handlePackagePostInstall(request, didRestore);
                 } else if (DEBUG_INSTALL) {
                     // No post-install when we run restore from installExistingPackageForUser
-                    Slog.i(TAG, "Nothing to do for post-install token " + msg.arg1);
+                    Slog.i(TAG, "Nothing to do for post-install token " + token);
                 }
 
-                Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "postInstall", msg.arg1);
+                Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "postInstall", token);
             } break;
             case DEFERRED_NO_KILL_POST_DELETE: {
                 CleanUpArgs args = (CleanUpArgs) msg.obj;
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index aaa4fdf..f60e086 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -960,6 +960,7 @@
     // Stores a list of users whose package restrictions file needs to be updated
     final ArraySet<Integer> mDirtyUsers = new ArraySet<>();
 
+    @GuardedBy("mRunningInstalls")
     final SparseArray<InstallRequest> mRunningInstalls = new SparseArray<>();
     int mNextInstallToken = 1;  // nonzero; will be wrapped back to 1 when ++ overflows
 
@@ -3291,20 +3292,23 @@
         // and been launched through some other means, so it is not in a problematic
         // state for observers to see the FIRST_LAUNCH signal.
         mHandler.post(() -> {
-            for (int i = 0; i < mRunningInstalls.size(); i++) {
-                final InstallRequest installRequest = mRunningInstalls.valueAt(i);
-                if (installRequest.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) {
-                    continue;
-                }
-                if (packageName.equals(installRequest.getPkg().getPackageName())) {
-                    // right package; but is it for the right user?
-                    for (int uIndex = 0; uIndex < installRequest.getNewUsers().length; uIndex++) {
-                        if (userId == installRequest.getNewUsers()[uIndex]) {
-                            if (DEBUG_BACKUP) {
-                                Slog.i(TAG, "Package " + packageName
-                                        + " being restored so deferring FIRST_LAUNCH");
+            synchronized (mRunningInstalls) {
+                for (int i = 0; i < mRunningInstalls.size(); i++) {
+                    final InstallRequest installRequest = mRunningInstalls.valueAt(i);
+                    if (installRequest.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) {
+                        continue;
+                    }
+                    final int[] newUsers = installRequest.getNewUsers();
+                    if (packageName.equals(installRequest.getPkg().getPackageName())) {
+                        // right package; but is it for the right user?
+                        for (int uIndex = 0; uIndex < newUsers.length; uIndex++) {
+                            if (userId == newUsers[uIndex]) {
+                                if (DEBUG_BACKUP) {
+                                    Slog.i(TAG, "Package " + packageName
+                                            + " being restored so deferring FIRST_LAUNCH");
+                                }
+                                return;
                             }
-                            return;
                         }
                     }
                 }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 23383a9..f9e4022 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -1395,7 +1395,9 @@
             DisplayGroupPowerChangeListener displayGroupPowerChangeListener =
                     new DisplayGroupPowerChangeListener();
             mDisplayManagerInternal.registerDisplayGroupListener(displayGroupPowerChangeListener);
-            mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler);
+            if (mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) {
+                mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler);
+            }
 
             if(mDreamManager != null){
                 // This DreamManager method does not acquire a lock, so it should be safe to call.
@@ -3852,6 +3854,10 @@
 
     @GuardedBy("mLock")
     private void notifyScreenTimeoutPolicyChangesLocked() {
+        if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) {
+            return;
+        }
+
         for (int idx = 0; idx < mPowerGroups.size(); idx++) {
             final int powerGroupId = mPowerGroups.keyAt(idx);
             final PowerGroup powerGroup = mPowerGroups.valueAt(idx);
@@ -6011,6 +6017,11 @@
         @Override // Binder call
         public void addScreenTimeoutPolicyListener(int displayId,
                 IScreenTimeoutPolicyListener listener) {
+            if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) {
+                throw new IllegalStateException("Screen timeout policy listener API flag "
+                        + "is not enabled");
+            }
+
             mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
                     null);
 
@@ -6042,6 +6053,11 @@
         @Override // Binder call
         public void removeScreenTimeoutPolicyListener(int displayId,
                 IScreenTimeoutPolicyListener listener) {
+            if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) {
+                throw new IllegalStateException("Screen timeout policy listener API flag "
+                        + "is not enabled");
+            }
+
             mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
                     null);
 
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
index 5cd7dee..42b4401 100644
--- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -37,6 +37,11 @@
             Flags.FLAG_ENABLE_EARLY_SCREEN_TIMEOUT_DETECTOR,
             Flags::enableEarlyScreenTimeoutDetector);
 
+    private final FlagState mEnableScreenTimeoutPolicyListenerApi = new FlagState(
+            Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API,
+            Flags::enableScreenTimeoutPolicyListenerApi
+    );
+
     private final FlagState mImproveWakelockLatency = new FlagState(
             Flags.FLAG_IMPROVE_WAKELOCK_LATENCY,
             Flags::improveWakelockLatency
@@ -63,6 +68,11 @@
         return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
     }
 
+    /** Returns whether screen timeout policy listener APIs are enabled on not. */
+    public boolean isScreenTimeoutPolicyListenerApiEnabled() {
+        return mEnableScreenTimeoutPolicyListenerApi.isEnabled();
+    }
+
     /**
      * @return Whether to improve the wakelock acquire/release latency or not
      */
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index a975da3..613daf8 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -12,6 +12,17 @@
 }
 
 flag {
+    name: "enable_screen_timeout_policy_listener_api"
+    namespace: "power"
+    description: "Enables APIs that allow to listen to screen timeout policy changes"
+    bug: "363174979"
+    is_fixed_read_only: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "improve_wakelock_latency"
     namespace: "power"
     description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock."
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
index 89fa9b6..b723da3 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsService.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.hardware.power.stats.Channel;
 import android.hardware.power.stats.EnergyConsumer;
 import android.hardware.power.stats.EnergyConsumerResult;
@@ -37,6 +38,7 @@
 import android.os.Looper;
 import android.os.PowerMonitor;
 import android.os.PowerMonitorReadings;
+import android.os.Process;
 import android.os.ResultReceiver;
 import android.os.UserHandle;
 import android.power.PowerStatsInternal;
@@ -83,7 +85,8 @@
     private static final String METER_CACHE_FILENAME = "meterCache";
     private static final String MODEL_CACHE_FILENAME = "modelCache";
     private static final String RESIDENCY_CACHE_FILENAME = "residencyCache";
-    private static final long MAX_POWER_MONITOR_AGE_MILLIS = 30_000;
+    private static final long MAX_POWER_MONITOR_AGE_MILLIS = 20_000;
+    private static final long MAX_FINE_POWER_MONITOR_AGE_MILLIS = 250;
 
     static final String KEY_POWER_MONITOR_API_ENABLED = "power_monitor_api_enabled";
 
@@ -203,6 +206,11 @@
         IntervalRandomNoiseGenerator createIntervalRandomNoiseGenerator() {
             return new IntervalRandomNoiseGenerator(INTERVAL_RANDOM_NOISE_GENERATION_ALPHA);
         }
+
+        boolean checkFinePowerMonitorsPermission(Context context, int callingUid) {
+            return context.checkPermission(android.Manifest.permission.ACCESS_FINE_POWER_MONITORS,
+                    Process.INVALID_PID, callingUid) == PackageManager.PERMISSION_GRANTED;
+        }
     }
 
     private final IBinder mService = new IPowerStatsService.Stub() {
@@ -571,6 +579,7 @@
     private boolean mPowerMonitorApiEnabled = true;
     private volatile PowerMonitor[] mPowerMonitors;
     private PowerMonitorState[] mPowerMonitorStates;
+    private PowerMonitorState[] mFinePowerMonitorStates;
     private IntervalRandomNoiseGenerator mIntervalRandomNoiseGenerator;
 
     private void setPowerMonitorApiEnabled(boolean powerMonitorApiEnabled) {
@@ -578,6 +587,7 @@
             mPowerMonitorApiEnabled = powerMonitorApiEnabled;
             mPowerMonitors = null;
             mPowerMonitorStates = null;
+            mFinePowerMonitorStates = null;
         }
     }
 
@@ -598,6 +608,7 @@
             if (!mPowerMonitorApiEnabled) {
                 mPowerMonitors = new PowerMonitor[0];
                 mPowerMonitorStates = new PowerMonitorState[0];
+                mFinePowerMonitorStates = new PowerMonitorState[0];
                 return;
             }
 
@@ -628,6 +639,7 @@
             }
             mPowerMonitors = monitors.toArray(new PowerMonitor[monitors.size()]);
             mPowerMonitorStates = states.toArray(new PowerMonitorState[monitors.size()]);
+            mFinePowerMonitorStates = states.toArray(new PowerMonitorState[monitors.size()]);
         }
     }
 
@@ -710,24 +722,38 @@
             ResultReceiver resultReceiver, int callingUid) {
         ensurePowerMonitors();
 
+        @PowerMonitorReadings.PowerMonitorGranularity int granularity =
+                mInjector.checkFinePowerMonitorsPermission(mContext, callingUid)
+                        ? PowerMonitorReadings.GRANULARITY_FINE
+                        : PowerMonitorReadings.GRANULARITY_UNSPECIFIED;
+
+        PowerMonitorState[] allPowerMonitorStates;
+        long maxAge;
+        if (granularity == PowerMonitorReadings.GRANULARITY_FINE) {
+            allPowerMonitorStates = mFinePowerMonitorStates;
+            maxAge = MAX_FINE_POWER_MONITOR_AGE_MILLIS;
+        } else {
+            allPowerMonitorStates = mPowerMonitorStates;
+            maxAge = MAX_POWER_MONITOR_AGE_MILLIS;
+        }
+
         long earliestTimestamp = Long.MAX_VALUE;
         PowerMonitorState[] powerMonitorStates = new PowerMonitorState[powerMonitorIndices.length];
         for (int i = 0; i < powerMonitorIndices.length; i++) {
             int index = powerMonitorIndices[i];
-            if (index < 0 || index >= mPowerMonitorStates.length) {
+            if (index < 0 || index >= allPowerMonitorStates.length) {
                 resultReceiver.send(IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR, null);
                 return;
             }
 
-            powerMonitorStates[i] = mPowerMonitorStates[index];
-            if (mPowerMonitorStates[index] != null
-                    && mPowerMonitorStates[index].timestampMs < earliestTimestamp) {
-                earliestTimestamp = mPowerMonitorStates[index].timestampMs;
+            powerMonitorStates[i] = allPowerMonitorStates[index];
+            if (allPowerMonitorStates[index] != null
+                    && allPowerMonitorStates[index].timestampMs < earliestTimestamp) {
+                earliestTimestamp = allPowerMonitorStates[index].timestampMs;
             }
         }
 
-        if (earliestTimestamp == 0
-                || mClock.elapsedRealtime() - earliestTimestamp > MAX_POWER_MONITOR_AGE_MILLIS) {
+        if (earliestTimestamp == 0 || mClock.elapsedRealtime() - earliestTimestamp > maxAge) {
             updateEnergyConsumers(powerMonitorStates);
             updateEnergyMeasurements(powerMonitorStates);
             mIntervalRandomNoiseGenerator.refresh();
@@ -765,6 +791,7 @@
         Bundle result = new Bundle();
         result.putLongArray(IPowerStatsService.KEY_ENERGY, energy);
         result.putLongArray(IPowerStatsService.KEY_TIMESTAMPS, timestamps);
+        result.putInt(IPowerStatsService.KEY_GRANULARITY, granularity);
         resultReceiver.send(IPowerStatsService.RESULT_SUCCESS, result);
     }
 
diff --git a/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java
new file mode 100644
index 0000000..2e73829
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java
@@ -0,0 +1,649 @@
+/*
+ * 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.timezonedetector;
+
+import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
+import static android.content.Context.RECEIVER_NOT_EXPORTED;
+import static android.provider.Settings.ACTION_DATE_SETTINGS;
+
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_UNKNOWN;
+
+import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
+import android.annotation.RequiresPermission;
+import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.icu.text.DateFormat;
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.TimeZone;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.server.LocalServices;
+import com.android.server.flags.Flags;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An implementation of {@link TimeZoneChangeListener} that fires notifications.
+ */
+public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener {
+    @IntDef({STATUS_UNKNOWN, STATUS_UNTRACKED, STATUS_REJECTED,
+            STATUS_ACCEPTED, STATUS_SUPERSEDED})
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+    @interface TimeZoneChangeStatus {}
+
+    /** Used to indicate the status could not be inferred. */
+    @TimeZoneChangeStatus
+    static final int STATUS_UNKNOWN = 0;
+    /** Used to indicate the change is not one that needs to be tracked. */
+    @TimeZoneChangeStatus
+    static final int STATUS_UNTRACKED = 1;
+    @TimeZoneChangeStatus
+    static final int STATUS_REJECTED = 2;
+    @TimeZoneChangeStatus
+    static final int STATUS_ACCEPTED = 3;
+    /** Used to indicate a change was superseded before its status could be determined. */
+    @TimeZoneChangeStatus
+    static final int STATUS_SUPERSEDED = 4;
+
+    @IntDef({SIGNAL_TYPE_UNKNOWN, SIGNAL_TYPE_NONE, SIGNAL_TYPE_NOTIFICATION,
+            SIGNAL_TYPE_HEURISTIC})
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+    @interface SignalType {}
+
+    /** Used when the signal type cannot be inferred. */
+    @SignalType
+    static final int SIGNAL_TYPE_UNKNOWN = 0;
+    /** Used when the status is not one that needs a signal type. */
+    @SignalType
+    static final int SIGNAL_TYPE_NONE = 1;
+    @SignalType
+    static final int SIGNAL_TYPE_NOTIFICATION = 2;
+    @SignalType
+    static final int SIGNAL_TYPE_HEURISTIC = 3;
+
+    private static final int MAX_EVENTS_TO_TRACK = 10;
+
+    @VisibleForTesting
+    @DurationMillisLong
+    static final long AUTO_REVERT_THRESHOLD = Duration.ofMinutes(15).toMillis();
+
+    private static final String TAG = "TimeZoneChangeTracker";
+    private static final String NOTIFICATION_TAG = "TimeZoneDetector";
+    private static final int TZ_CHANGE_NOTIFICATION_ID = 1001;
+
+    private static final String ACTION_NOTIFICATION_DELETED =
+            "com.android.server.timezonedetector.TimeZoneNotificationDeleted";
+
+    private static final String NOTIFICATION_INTENT_EXTRA_USER_ID = "user_id";
+    private static final String NOTIFICATION_INTENT_EXTRA_CHANGE_ID = "change_id";
+
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    private final ActivityManagerInternal mActivityManagerInternal;
+
+    // For scheduling callbacks
+    private final Handler mHandler;
+    private final ServiceConfigAccessor mServiceConfigAccessor;
+    private final AtomicInteger mNextChangeEventId = new AtomicInteger(1);
+
+    private final Resources mRes = Resources.getSystem();
+
+    @GuardedBy("mTimeZoneChangeRecord")
+    private final ReferenceWithHistory<TimeZoneChangeRecord> mTimeZoneChangeRecord =
+            new ReferenceWithHistory<>(MAX_EVENTS_TO_TRACK);
+
+    private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case ACTION_NOTIFICATION_DELETED:
+                    int notifiedUserId = intent.getIntExtra(
+                            NOTIFICATION_INTENT_EXTRA_USER_ID, UserHandle.USER_NULL);
+                    int changeEventId = intent.getIntExtra(
+                            NOTIFICATION_INTENT_EXTRA_CHANGE_ID, 0);
+                    notificationSwipedAway(notifiedUserId, changeEventId);
+                    break;
+                default:
+                    Log.d(TAG, "Unknown intent action received: " + intent.getAction());
+            }
+        }
+    };
+
+    private final Object mConfigurationLock = new Object();
+    @GuardedBy("mConfigurationLock")
+    private ConfigurationInternal mConfigurationInternal;
+    @GuardedBy("mConfigurationLock")
+    private boolean mIsRegistered;
+
+    private int mAcceptedManualChanges;
+    private int mAcceptedTelephonyChanges;
+    private int mAcceptedLocationChanges;
+    private int mAcceptedUnknownChanges;
+    private int mRejectedTelephonyChanges;
+    private int mRejectedLocationChanges;
+    private int mRejectedUnknownChanges;
+
+    /** Create and initialise a new {@code TimeZoneChangeTrackerImpl} */
+    @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL")
+    public static NotifyingTimeZoneChangeListener create(Handler handler, Context context,
+            ServiceConfigAccessor serviceConfigAccessor) {
+        NotifyingTimeZoneChangeListener changeTracker =
+                new NotifyingTimeZoneChangeListener(handler,
+                        context,
+                        serviceConfigAccessor,
+                        context.getSystemService(NotificationManager.class));
+
+        // Pretend there was an update to initialize configuration.
+        changeTracker.handleConfigurationUpdate();
+
+        return changeTracker;
+    }
+
+    @VisibleForTesting
+    NotifyingTimeZoneChangeListener(
+            Handler handler, Context context, ServiceConfigAccessor serviceConfigAccessor,
+            NotificationManager notificationManager) {
+        mHandler = Objects.requireNonNull(handler);
+        mContext = Objects.requireNonNull(context);
+        mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
+        mServiceConfigAccessor.addConfigurationInternalChangeListener(
+                this::handleConfigurationUpdate);
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+        mNotificationManager = notificationManager;
+    }
+
+    @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL")
+    private void handleConfigurationUpdate() {
+        synchronized (mConfigurationLock) {
+            ConfigurationInternal oldConfigurationInternal = mConfigurationInternal;
+            mConfigurationInternal = mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+
+            if (areNotificationsEnabled() && isNotificationTrackingSupported()) {
+                if (!mIsRegistered) {
+                    IntentFilter intentFilter = new IntentFilter();
+                    intentFilter.addAction(ACTION_NOTIFICATION_DELETED);
+                    mContext.registerReceiverForAllUsers(mNotificationReceiver, intentFilter,
+                            /* broadcastPermission= */ null, mHandler, RECEIVER_NOT_EXPORTED);
+                    mIsRegistered = true;
+                }
+            } else if (mIsRegistered) {
+                mContext.unregisterReceiver(mNotificationReceiver);
+                mIsRegistered = false;
+            }
+
+            if (oldConfigurationInternal != null) {
+                boolean userChanged =
+                        oldConfigurationInternal.getUserId() != mConfigurationInternal.getUserId();
+
+                if (!areNotificationsEnabled() || userChanged) {
+                    // Clear any notifications that are no longer needed.
+                    clearNotificationForUser(oldConfigurationInternal.getUserId());
+                }
+            }
+        }
+    }
+
+    private void notificationSwipedAway(@UserIdInt int userId, int changeEventId) {
+        // User swiping away a notification is interpreted as "user accepted the change".
+        if (isNotificationTrackingSupported()) {
+            markChangeAsAccepted(changeEventId, userId, SIGNAL_TYPE_NOTIFICATION);
+        }
+    }
+
+    private boolean areNotificationsEnabled() {
+        synchronized (mConfigurationLock) {
+            return mConfigurationInternal.getNotificationsEnabledBehavior();
+        }
+    }
+
+    private boolean isNotificationTrackingSupported() {
+        synchronized (mConfigurationLock) {
+            return mConfigurationInternal.isNotificationTrackingSupported();
+        }
+    }
+
+    private boolean isManualChangeTrackingSupported() {
+        synchronized (mConfigurationLock) {
+            return mConfigurationInternal.isManualChangeTrackingSupported();
+        }
+    }
+
+    /**
+     * Marks a change event as accepted by the user
+     *
+     * <p>A change event is said to be accepted when the client does not revert an automatic time
+     * zone change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the
+     * notification being received.
+     */
+    private void markChangeAsAccepted(int changeEventId, @UserIdInt int userId,
+            @SignalType int signalType) {
+        if (!isUserIdCurrentUser(userId)) {
+            return;
+        }
+
+        synchronized (mTimeZoneChangeRecord) {
+            TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get();
+            if (lastTimeZoneChangeRecord != null) {
+                if (lastTimeZoneChangeRecord.getId() != changeEventId) {
+                    // To be accepted, the change being accepted has to still be the latest.
+                    return;
+                }
+                if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) {
+                    // Change status has already been set.
+                    return;
+                }
+                lastTimeZoneChangeRecord.setAccepted(signalType);
+
+                switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) {
+                    case ORIGIN_MANUAL:
+                        mAcceptedManualChanges += 1;
+                        break;
+                    case ORIGIN_TELEPHONY:
+                        mAcceptedTelephonyChanges += 1;
+                        break;
+                    case ORIGIN_LOCATION:
+                        mAcceptedLocationChanges += 1;
+                        break;
+                    default:
+                        mAcceptedUnknownChanges += 1;
+                        break;
+                }
+            }
+        }
+    }
+
+    private boolean isUserIdCurrentUser(@UserIdInt int userId) {
+        synchronized (mConfigurationLock) {
+            return userId == mConfigurationInternal.getUserId();
+        }
+    }
+
+    /**
+     * Marks a change event as rejected by the user
+     *
+     * <p>A change event is said to be rejected when the client reverts an automatic time zone
+     * change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the
+     * notification being received.
+     */
+    @GuardedBy("mTimeZoneChangeRecord")
+    private void markChangeAsRejected(int changeEventId, @UserIdInt int userId,
+            @SignalType int signalType) {
+        if (!isUserIdCurrentUser(userId)) {
+            return;
+        }
+
+        TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get();
+        if (lastTimeZoneChangeRecord != null) {
+            if (lastTimeZoneChangeRecord.getId() != changeEventId) {
+                // To be accepted, the change being accepted has to still be the latest.
+                return;
+            }
+            if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) {
+                // Change status has already been set.
+                return;
+            }
+            lastTimeZoneChangeRecord.setRejected(signalType);
+
+            switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) {
+                case ORIGIN_TELEPHONY:
+                    mRejectedTelephonyChanges += 1;
+                    break;
+                case ORIGIN_LOCATION:
+                    mRejectedLocationChanges += 1;
+                    break;
+                default:
+                    mRejectedUnknownChanges += 1;
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void process(TimeZoneChangeEvent changeEvent) {
+        final TimeZoneChangeRecord trackedChangeEvent;
+
+        synchronized (mTimeZoneChangeRecord) {
+            fixPotentialHistoryCorruption(changeEvent);
+
+            TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get();
+            int changeEventId = mNextChangeEventId.getAndIncrement();
+            trackedChangeEvent = new TimeZoneChangeRecord(changeEventId, changeEvent);
+
+            if (isManualChangeTrackingSupported()) {
+                // Time-based heuristic for "user is undoing a mistake made by the time zone
+                // detector".
+                if (lastTimeZoneChangeRecord != null
+                        && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) {
+                    TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent();
+
+                    if (shouldRejectChangeEvent(changeEvent, lastChangeEvent)) {
+                        markChangeAsRejected(lastTimeZoneChangeRecord.getId(),
+                                changeEvent.getUserId(), SIGNAL_TYPE_HEURISTIC);
+                    }
+                }
+
+                // Schedule a callback for the new time zone so that we can implement "user accepted
+                // the change because they didn't revert it"
+                scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent, AUTO_REVERT_THRESHOLD);
+            }
+
+            if (lastTimeZoneChangeRecord != null
+                    && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) {
+                lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE);
+            }
+
+            if (changeEvent.getOrigin() == ORIGIN_MANUAL) {
+                trackedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+            }
+
+            mTimeZoneChangeRecord.set(trackedChangeEvent);
+        }
+
+        if (areNotificationsEnabled()) {
+            int currentUserId;
+            synchronized (mConfigurationLock) {
+                currentUserId = mConfigurationInternal.getUserId();
+            }
+
+            if (changeEvent.getOrigin() == ORIGIN_MANUAL) {
+                // Just clear any existing notification.
+                clearNotificationForUser(currentUserId);
+            } else {
+                notifyOfTimeZoneChange(currentUserId, trackedChangeEvent);
+            }
+        }
+    }
+
+    /**
+     * Checks if the history of time zone change events is corrupted and fixes it, if needed
+     *
+     * <p>The history of changes is considered corrupted if a transition is missing. That is, if
+     * {@code events[i-1].newTimeZoneId != events[i].oldTimeZoneId}. In that case, a "synthetic"
+     * event is added to the history to bridge the gap between the last reported time zone ID and
+     * the time zone ID that the new event is replacing.
+     *
+     * <p>Note: we are not expecting this method to be required often (if ever) but in the
+     * eventuality that an event gets lost, we want to keep the history coherent.
+     */
+    @GuardedBy("mTimeZoneChangeRecord")
+    private void fixPotentialHistoryCorruption(TimeZoneChangeEvent changeEvent) {
+        TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get();
+
+        if (lastTimeZoneChangeRecord != null) {
+            // The below block takes care of the case where we are missing record(s) of time
+            // zone changes
+            TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent();
+            if (!changeEvent.getOldZoneId().equals(lastChangeEvent.getNewZoneId())) {
+                int changeEventId = mNextChangeEventId.getAndIncrement();
+                TimeZoneChangeEvent syntheticChangeEvent = new TimeZoneChangeEvent(
+                        SystemClock.elapsedRealtime(), System.currentTimeMillis(),
+                        ORIGIN_UNKNOWN, UserHandle.USER_NULL, lastChangeEvent.getNewZoneId(),
+                        changeEvent.getOldZoneId(), 0, "Synthetic");
+                TimeZoneChangeRecord syntheticTrackedChangeEvent =
+                        new TimeZoneChangeRecord(changeEventId, syntheticChangeEvent);
+                syntheticTrackedChangeEvent.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE);
+
+                mTimeZoneChangeRecord.set(syntheticTrackedChangeEvent);
+
+                // Housekeeping for the last reported time zone change: try to ensure it has
+                // a status too.
+                if (lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) {
+                    lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE);
+                }
+            }
+        }
+    }
+
+    private static boolean shouldRejectChangeEvent(TimeZoneChangeEvent changeEvent,
+            TimeZoneChangeEvent lastChangeEvent) {
+        return changeEvent.getOrigin() == ORIGIN_MANUAL
+                && lastChangeEvent.getOrigin() != ORIGIN_MANUAL
+                && (changeEvent.getElapsedRealtimeMillis()
+                - lastChangeEvent.getElapsedRealtimeMillis() < AUTO_REVERT_THRESHOLD);
+    }
+
+    private void scheduleChangeAcceptedHeuristicCallback(
+            TimeZoneChangeRecord trackedChangeEvent,
+            @DurationMillisLong long delayMillis) {
+        mHandler.postDelayed(
+                () -> changeAcceptedTimeHeuristicCallback(trackedChangeEvent.getId()), delayMillis);
+    }
+
+    private void changeAcceptedTimeHeuristicCallback(int changeEventId) {
+        if (isManualChangeTrackingSupported()) {
+            int currentUserId = mActivityManagerInternal.getCurrentUserId();
+            markChangeAsAccepted(changeEventId, currentUserId, SIGNAL_TYPE_HEURISTIC);
+        }
+    }
+
+    private void clearNotificationForUser(@UserIdInt int userId) {
+        mNotificationManager.cancelAsUser(NOTIFICATION_TAG, TZ_CHANGE_NOTIFICATION_ID,
+                UserHandle.of(userId));
+    }
+
+    private void notifyOfTimeZoneChange(@UserIdInt int userId,
+            TimeZoneChangeRecord trackedChangeEvent) {
+        TimeZoneChangeEvent changeEvent = trackedChangeEvent.getEvent();
+
+        if (!Flags.datetimeNotifications() || !areNotificationsEnabled()) {
+            return;
+        }
+
+        TimeZone oldTimeZone = TimeZone.getTimeZone(changeEvent.getOldZoneId());
+        TimeZone newTimeZone = TimeZone.getTimeZone(changeEvent.getNewZoneId());
+        long unixEpochTimeMillis = changeEvent.getUnixEpochTimeMillis();
+        boolean hasOffsetChanged = newTimeZone.getOffset(unixEpochTimeMillis)
+                == oldTimeZone.getOffset(unixEpochTimeMillis);
+
+        if (hasOffsetChanged) {
+            // If the time zone ID changes but not the offset, we do not send a notification to
+            // the user. This is to prevent spamming users and reduce the number of notification
+            // we send overall.
+            Log.d(TAG, "The time zone ID has changed but the offset remains the same.");
+            return;
+        }
+
+        final CharSequence title = mRes.getString(R.string.time_zone_change_notification_title);
+        final CharSequence body = getNotificationBody(newTimeZone, unixEpochTimeMillis);
+
+        final Intent clickNotificationIntent = new Intent(ACTION_DATE_SETTINGS)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                        | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+                        | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+        final Intent clearNotificationIntent = new Intent(ACTION_NOTIFICATION_DELETED)
+                .putExtra(NOTIFICATION_INTENT_EXTRA_USER_ID, userId)
+                .putExtra(NOTIFICATION_INTENT_EXTRA_CHANGE_ID, trackedChangeEvent.getId());
+
+        Notification notification = new Notification.Builder(mContext,
+                SystemNotificationChannels.TIME)
+                .setSmallIcon(R.drawable.btn_clock_material)
+                .setStyle(new Notification.BigTextStyle().bigText(body))
+                .setOnlyAlertOnce(true)
+                .setColor(mContext.getColor(R.color.system_notification_accent_color))
+                .setTicker(title)
+                .setContentTitle(title)
+                .setContentText(body)
+                .setContentIntent(PendingIntent.getActivityAsUser(
+                        mContext,
+                        /* requestCode= */ 0,
+                        clickNotificationIntent,
+                        /* flags= */ FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
+                        /* options= */ null,
+                        UserHandle.of(userId)))
+                .setDeleteIntent(PendingIntent.getBroadcast(
+                        mContext,
+                        /* requestCode= */ 0,
+                        clearNotificationIntent,
+                        /* flags= */ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE))
+                .setAutoCancel(true) // auto-clear notification on selection
+                .build();
+
+        mNotificationManager.notifyAsUser(NOTIFICATION_TAG,
+                TZ_CHANGE_NOTIFICATION_ID, notification, UserHandle.of(userId));
+    }
+
+    private CharSequence getNotificationBody(TimeZone newTimeZone, long unixEpochTimeMillis) {
+        DateFormat timeFormat = SimpleDateFormat.getInstanceForSkeleton("zzzz");
+        DateFormat offsetFormat = SimpleDateFormat.getInstanceForSkeleton("ZZZZ");
+
+        String newTime = formatInZone(timeFormat, newTimeZone, unixEpochTimeMillis);
+        String newOffset = formatInZone(offsetFormat, newTimeZone, unixEpochTimeMillis);
+
+        return mRes.getString(R.string.time_zone_change_notification_body, newTime, newOffset);
+    }
+
+    private static String formatInZone(DateFormat timeFormat, TimeZone timeZone,
+            long unixEpochTimeMillis) {
+        timeFormat.setTimeZone(timeZone);
+        return timeFormat.format(unixEpochTimeMillis);
+    }
+
+    @Override
+    public void dump(IndentingPrintWriter pw) {
+        synchronized (mConfigurationLock) {
+            pw.println("currentUserId=" + mConfigurationInternal.getUserId());
+            pw.println("notificationsEnabledBehavior="
+                    + mConfigurationInternal.getNotificationsEnabledBehavior());
+            pw.println("notificationTrackingSupported="
+                    + mConfigurationInternal.isNotificationTrackingSupported());
+            pw.println("manualChangeTrackingSupported="
+                    + mConfigurationInternal.isManualChangeTrackingSupported());
+        }
+
+        pw.println("mAcceptedLocationChanges=" + mAcceptedLocationChanges);
+        pw.println("mAcceptedManualChanges=" + mAcceptedManualChanges);
+        pw.println("mAcceptedTelephonyChanges=" + mAcceptedTelephonyChanges);
+        pw.println("mAcceptedUnknownChanges=" + mAcceptedUnknownChanges);
+        pw.println("mRejectedLocationChanges=" + mRejectedLocationChanges);
+        pw.println("mRejectedTelephonyChanges=" + mRejectedTelephonyChanges);
+        pw.println("mRejectedUnknownChanges=" + mRejectedUnknownChanges);
+        pw.println("mNextChangeEventId=" + mNextChangeEventId);
+
+        pw.println("mTimeZoneChangeRecord:");
+        pw.increaseIndent();
+        synchronized (mTimeZoneChangeRecord) {
+            mTimeZoneChangeRecord.dump(pw);
+        }
+        pw.decreaseIndent();
+    }
+
+    @VisibleForTesting
+    static class TimeZoneChangeRecord {
+
+        private final int mId;
+        private final TimeZoneChangeEvent mEvent;
+        private @TimeZoneChangeStatus int mStatus = STATUS_UNKNOWN;
+        private @SignalType int mSignalType = SIGNAL_TYPE_UNKNOWN;
+
+        TimeZoneChangeRecord(int id, TimeZoneChangeEvent event) {
+            mId = id;
+            mEvent = Objects.requireNonNull(event);
+        }
+
+        public int getId() {
+            return mId;
+        }
+
+        public @TimeZoneChangeStatus int getStatus() {
+            return mStatus;
+        }
+
+        public void setAccepted(int signalType) {
+            setStatus(STATUS_ACCEPTED, signalType);
+        }
+
+        public void setRejected(int signalType) {
+            setStatus(STATUS_REJECTED, signalType);
+        }
+
+        public void setStatus(@TimeZoneChangeStatus int status, @SignalType int signalType) {
+            mStatus = status;
+            mSignalType = signalType;
+        }
+
+        public TimeZoneChangeEvent getEvent() {
+            return mEvent;
+        }
+
+        @Override
+        public String toString() {
+            return "TrackedTimeZoneChangeEvent{"
+                    + "mId=" + mId
+                    + ", mEvent=" + mEvent
+                    + ", mStatus=" + mStatus
+                    + ", mSignalType=" + mSignalType
+                    + '}';
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o instanceof TimeZoneChangeRecord that) {
+                return mId == that.mId
+                        && mEvent.equals(that.mEvent)
+                        && mStatus == that.mStatus
+                        && mSignalType == that.mSignalType;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mId, mEvent, mStatus, mSignalType);
+        }
+    }
+
+    @VisibleForTesting
+    TimeZoneChangeRecord getLastTimeZoneChangeRecord() {
+        synchronized (mTimeZoneChangeRecord) {
+            return mTimeZoneChangeRecord.get();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java
index e14326c..d340ed4 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java
@@ -102,5 +102,29 @@
                     + ", mCause='" + mCause + '\''
                     + '}';
         }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o instanceof TimeZoneChangeEvent that) {
+                return mElapsedRealtimeMillis == that.mElapsedRealtimeMillis
+                        && mUnixEpochTimeMillis == that.mUnixEpochTimeMillis
+                        && mOrigin == that.mOrigin
+                        && mUserId == that.mUserId
+                        && Objects.equals(mOldZoneId, that.mOldZoneId)
+                        && Objects.equals(mNewZoneId, that.mNewZoneId)
+                        && mNewConfidence == that.mNewConfidence
+                        && Objects.equals(mCause, that.mCause);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mElapsedRealtimeMillis, mUnixEpochTimeMillis, mOrigin, mUserId,
+                    mOldZoneId, mNewZoneId, mNewConfidence, mCause);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
index 19a28dd..b2b06b0 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
@@ -42,6 +42,7 @@
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
 import android.content.Context;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.os.TimestampedValue;
 import android.os.UserHandle;
 import android.util.IndentingPrintWriter;
@@ -50,6 +51,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.SystemTimeZone.TimeZoneConfidence;
+import com.android.server.flags.Flags;
 import com.android.server.timezonedetector.ConfigurationInternal.DetectionMode;
 
 import java.io.PrintWriter;
@@ -215,6 +217,13 @@
     private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>();
 
     /**
+     * A component adjunct to the detection behavior that tracks time zone changes and implements
+     * behavior associated with time zone changes.
+     */
+    @NonNull
+    private final TimeZoneChangeListener mChangeTracker;
+
+    /**
      * A snapshot of the current detector status. A local copy is cached because it is relatively
      * heavyweight to obtain and is used more often than it is expected to change.
      */
@@ -256,15 +265,20 @@
             @NonNull ServiceConfigAccessor serviceConfigAccessor) {
 
         Environment environment = new EnvironmentImpl(handler);
-        return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, environment);
+        TimeZoneChangeListener changeEventTracker =
+                NotifyingTimeZoneChangeListener.create(handler, context, serviceConfigAccessor);
+        return new TimeZoneDetectorStrategyImpl(
+                serviceConfigAccessor, environment, changeEventTracker);
     }
 
     @VisibleForTesting
     public TimeZoneDetectorStrategyImpl(
             @NonNull ServiceConfigAccessor serviceConfigAccessor,
-            @NonNull Environment environment) {
+            @NonNull Environment environment,
+            @NonNull TimeZoneChangeListener changeEventTracker) {
         mEnvironment = Objects.requireNonNull(environment);
         mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
+        mChangeTracker = Objects.requireNonNull(changeEventTracker);
 
         // Start with telephony fallback enabled.
         mTelephonyTimeZoneFallbackEnabled =
@@ -833,6 +847,17 @@
             Slog.d(LOG_TAG, logInfo);
         }
         mEnvironment.setDeviceTimeZoneAndConfidence(newZoneId, newConfidence, logInfo);
+
+        if (Flags.datetimeNotifications()) {
+            // Record the fact that the time zone was changed so that it can be tracked, i.e.
+            // whether the device / user sticks with it.
+            TimeZoneChangeListener.TimeZoneChangeEvent changeEvent =
+                    new TimeZoneChangeListener.TimeZoneChangeEvent(
+                            SystemClock.elapsedRealtime(), System.currentTimeMillis(), origin,
+                            userId,
+                            currentZoneId, newZoneId, newConfidence, cause);
+            mChangeTracker.process(changeEvent);
+        }
     }
 
     @GuardedBy("this")
@@ -947,6 +972,14 @@
         ipw.increaseIndent(); // level 2
         mTelephonySuggestionsBySlotIndex.dump(ipw);
         ipw.decreaseIndent(); // level 2
+
+        if (Flags.datetimeNotifications()) {
+            ipw.println("Time zone change tracker:");
+            ipw.increaseIndent(); // level 2
+            mChangeTracker.dump(ipw);
+            ipw.decreaseIndent(); // level 2
+        }
+
         ipw.decreaseIndent(); // level 1
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 1fe6159..d31aed2 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8995,13 +8995,12 @@
                 || orientationRespectedWithInsets)) {
             return;
         }
-        final AppCompatDisplayInsets mAppCompatDisplayInsets = getAppCompatDisplayInsets();
+        final AppCompatDisplayInsets appCompatDisplayInsets = getAppCompatDisplayInsets();
         final AppCompatSizeCompatModePolicy scmPolicy =
                 mAppCompatController.getAppCompatSizeCompatModePolicy();
 
-        if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()
-                && mAppCompatDisplayInsets != null
-                && !mAppCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
+        if (appCompatDisplayInsets != null
+                && !appCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
             // App prefers to keep its original size.
             // If the size compat is from previous fixed orientation letterboxing, we may want to
             // have fixed orientation letterbox again, otherwise it will show the size compat
@@ -9053,8 +9052,8 @@
                 .applyDesiredAspectRatio(newParentConfig, parentBounds, resolvedBounds,
                         containingBoundsWithInsets, containingBounds);
 
-        if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) {
-            mAppCompatDisplayInsets.getBoundsByRotation(mTmpBounds,
+        if (appCompatDisplayInsets != null) {
+            appCompatDisplayInsets.getBoundsByRotation(mTmpBounds,
                     newParentConfig.windowConfiguration.getRotation());
             if (resolvedBounds.width() != mTmpBounds.width()
                     || resolvedBounds.height() != mTmpBounds.height()) {
@@ -9077,7 +9076,7 @@
 
         // Calculate app bounds using fixed orientation bounds because they will be needed later
         // for comparison with size compat app bounds in {@link resolveSizeCompatModeConfiguration}.
-        mResolveConfigHint.mTmpCompatInsets = mAppCompatDisplayInsets;
+        mResolveConfigHint.mTmpCompatInsets = appCompatDisplayInsets;
         computeConfigByResolveHint(getResolvedOverrideConfiguration(), newParentConfig);
         mAppCompatController.getAppCompatAspectRatioPolicy()
                 .setLetterboxBoundsForFixedOrientationAndAspectRatio(new Rect(resolvedBounds));
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index ef6f923..12c8f9c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2538,7 +2538,7 @@
 
     void wakeUp(int displayId, String reason) {
         mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_APPLICATION,
-                "android.server.am:TURN_ON:" + reason, displayId);
+                "android.server.wm:TURN_ON:" + reason, displayId);
     }
 
     /** Starts a batch of visibility updates. */
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 9c1cf6e..fe478c60 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5230,9 +5230,15 @@
             // to ensure any necessary pause logic occurs. In the case where the Activity will be
             // shown regardless of the lock screen, the call to
             // {@link ActivityTaskSupervisor#checkReadyForSleepLocked} is skipped.
-            final ActivityRecord next = topRunningActivity(true /* focusableOnly */);
-            if (next == null || !next.canTurnScreenOn()) {
-                checkReadyForSleep();
+            if (shouldSleepActivities()) {
+                final ActivityRecord next = topRunningActivity(true /* focusableOnly */);
+                if (next != null && next.canTurnScreenOn()
+                        && !mWmService.mPowerManager.isInteractive()) {
+                    mTaskSupervisor.wakeUp(getDisplayId(), "resumeTop-turnScreenOnFlag");
+                    next.setCurrentLaunchCanTurnScreenOn(false);
+                } else {
+                    checkReadyForSleep();
+                }
             }
         } finally {
             mInResumeTopActivity = false;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 54a3d41..e761e02 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -117,6 +117,7 @@
 import com.android.server.wm.SurfaceAnimator.AnimationType;
 import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
 import com.android.server.wm.utils.AlwaysTruePredicate;
+import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
@@ -2736,6 +2737,13 @@
         if (!mTransitionController.canAssignLayers(this)) return;
         final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null;
         if (mSurfaceControl != null && changed) {
+            if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) {
+                // When this container needs to be synced, assign layer with its own sync
+                // transaction to avoid out of ordering when merge.
+                // Still use the passed-in transaction for non-sync case, such as building finish
+                // transaction.
+                t = getSyncTransaction();
+            }
             setLayer(t, layer);
             mLastLayer = layer;
             mLastRelativeToLayer = null;
@@ -2746,6 +2754,13 @@
             boolean forceUpdate) {
         final boolean changed = layer != mLastLayer || mLastRelativeToLayer != relativeTo;
         if (mSurfaceControl != null && (changed || forceUpdate)) {
+            if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) {
+                // When this container needs to be synced, assign layer with its own sync
+                // transaction to avoid out of ordering when merge.
+                // Still use the passed-in transaction for non-sync case, such as building finish
+                // transaction.
+                t = getSyncTransaction();
+            }
             setRelativeLayer(t, relativeTo, layer);
             mLastLayer = layer;
             mLastRelativeToLayer = relativeTo;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index d69b06a..85e3d89 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -5562,6 +5562,13 @@
     @Override
     void assignLayer(Transaction t, int layer) {
         if (mStartingData != null) {
+            if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) {
+                // When this container needs to be synced, assign layer with its own sync
+                // transaction to avoid out of ordering when merge.
+                // Still use the passed-in transaction for non-sync case, such as building finish
+                // transaction.
+                t = getSyncTransaction();
+            }
             // The starting window should cover the task.
             t.setLayer(mSurfaceControl, Integer.MAX_VALUE);
             return;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index d2d3884..0ce25db 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -10470,6 +10470,7 @@
         // Reset some of the user-specific policies.
         final DevicePolicyData policy = getUserData(userId);
         policy.mPermissionPolicy = DevicePolicyManager.PERMISSION_POLICY_PROMPT;
+        mPolicyCache.setPermissionPolicy(userId, policy.mPermissionPolicy);
         // Clear delegations.
         policy.mDelegationMap.clear();
         policy.mStatusBarDisabled = false;
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index a8e6f68..dae481a 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -1999,9 +1999,9 @@
         // Create new lib file without signature info
         incfs::NewFileParams libFileParams = {
                 .size = entry.uncompressed_length,
-                .signature = {},
                 // Metadata of the new lib file is its relative path
                 .metadata = {targetLibPath.c_str(), (IncFsSize)targetLibPath.size()},
+                .signature = {},
         };
         incfs::FileId libFileId = idFromMetadata(targetLibPath);
         if (auto res = mIncFs->makeFile(ifs->control, targetLibPathAbsolute, 0755, libFileId,
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 8e06ed8..fadab1f 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -3130,13 +3130,8 @@
                             && context.getPackageManager().hasSystemFeature(
                                     PackageManager.FEATURE_BLUETOOTH_LE))) {
                 t.traceBegin("RangingService");
-                // TODO: b/375264320 - Remove after RELEASE_RANGING_STACK is ramped to next.
-                try {
-                    mSystemServiceManager.startServiceFromJar(RANGING_SERVICE_CLASS,
-                            RANGING_APEX_SERVICE_JAR_PATH);
-                } catch (Throwable e) {
-                    Slog.d(TAG, "service-ranging.jar not found, not starting RangingService");
-                }
+                mSystemServiceManager.startServiceFromJar(RANGING_SERVICE_CLASS,
+                        RANGING_APEX_SERVICE_JAR_PATH);
                 t.traceEnd();
             }
         }
diff --git a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
index b80d68d..ba64ed4 100644
--- a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
@@ -1518,14 +1518,13 @@
     }
 
     /**
-     * Test verifying that {@link BackupManagerService#MORE_DEBUG} is set to {@code false}. This is
+     * Test verifying that {@link BackupManagerService#DEBUG} is set to {@code false}. This is
      * specifically to prevent overloading the logs in production.
      */
     @Test
-    public void testMoreDebug_isFalse() throws Exception {
-        boolean moreDebug = BackupManagerService.MORE_DEBUG;
-
-        assertThat(moreDebug).isFalse();
+    public void testDebug_isFalse() {
+        boolean debug = BackupManagerService.DEBUG;
+        assertThat(debug).isFalse();
     }
 
     /** Test that the constructor handles {@code null} parameters. */
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java
index 2db2438..2749b0a 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java
@@ -57,8 +57,8 @@
     }
 
     @Test
-    public void testMoreDebug_isFalse() {
-        assertThat(KeyValueBackupReporter.MORE_DEBUG).isFalse();
+    public void testDebug_isFalse() {
+        assertThat(KeyValueBackupReporter.DEBUG).isFalse();
     }
 
     @Test
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
index de16b7e..2dd16f6 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
@@ -109,6 +109,7 @@
 import com.android.server.LocalServices;
 import com.android.server.backup.BackupAgentConnectionManager;
 import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.BackupWakeLock;
 import com.android.server.backup.DataChangedJournal;
 import com.android.server.backup.KeyValueBackupJob;
 import com.android.server.backup.PackageManagerBackupAgent;
@@ -201,7 +202,7 @@
     private TransportData mTransport;
     private ShadowLooper mShadowBackupLooper;
     private Handler mBackupHandler;
-    private UserBackupManagerService.BackupWakeLock mWakeLock;
+    private BackupWakeLock mWakeLock;
     private KeyValueBackupReporter mReporter;
     private PackageManager mPackageManager;
     private ShadowPackageManager mShadowPackageManager;
@@ -238,7 +239,7 @@
         mPackageManager = mApplication.getPackageManager();
         mShadowPackageManager = shadowOf(mPackageManager);
 
-        mWakeLock = createBackupWakeLock(mApplication);
+        mWakeLock = spy(createBackupWakeLock(mApplication));
         mBackupManager = spy(FakeIBackupManager.class);
 
         // Needed to be able to use a real BMS instead of a mock
@@ -737,17 +738,16 @@
                     // In production (for non-system agents) the call is asynchronous, but here is
                     // synchronous, so it's fine to verify here.
                     // Verify has set work source and hasn't unset yet.
-                    verify(mBackupManagerService)
-                            .setWorkSource(
-                                    argThat(workSource -> workSource.getUid(0) == PACKAGE_1.uid));
-                    verify(mBackupManagerService, never()).setWorkSource(null);
+                    verify(mWakeLock).setWorkSource(
+                            argThat(workSource -> workSource.getUid(0) == PACKAGE_1.uid));
+                    verify(mWakeLock, never()).setWorkSource(null);
                 });
         KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
 
         runTask(task);
 
         // More verifications inside agent call above
-        verify(mBackupManagerService).setWorkSource(null);
+        verify(mWakeLock).setWorkSource(null);
     }
 
     /**
@@ -765,7 +765,7 @@
 
         runTask(task);
 
-        verify(mBackupManagerService).setWorkSource(null);
+        verify(mWakeLock).setWorkSource(null);
         verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
         verify(mObserver).backupFinished(SUCCESS);
         assertBackupPendingFor(PACKAGE_1);
@@ -798,7 +798,7 @@
 
         runTask(task);
 
-        verify(mBackupManagerService).setWorkSource(null);
+        verify(mWakeLock).setWorkSource(null);
         verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
         verify(mObserver).backupFinished(SUCCESS);
         assertBackupPendingFor(PACKAGE_1);
@@ -815,7 +815,7 @@
 
         runTask(task);
 
-        verify(mBackupManagerService).setWorkSource(null);
+        verify(mWakeLock).setWorkSource(null);
         verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
         verify(mObserver).backupFinished(SUCCESS);
         assertBackupPendingFor(PACKAGE_1);
@@ -833,7 +833,7 @@
 
         runTask(task);
 
-        verify(mBackupManagerService).setWorkSource(null);
+        verify(mWakeLock).setWorkSource(null);
         verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE);
         verify(mObserver).backupFinished(SUCCESS);
         assertBackupPendingFor(PACKAGE_1);
@@ -864,7 +864,7 @@
 
         runTask(task);
 
-        verify(mBackupManagerService).setWorkSource(null);
+        verify(mWakeLock).setWorkSource(null);
         verify(mBackupAgentConnectionManager).unbindAgent(argThat(applicationInfo(PACKAGE_1)),
                 eq(false));
     }
@@ -918,7 +918,7 @@
 
         runTask(task);
 
-        verify(mBackupManagerService).setWorkSource(null);
+        verify(mWakeLock).setWorkSource(null);
     }
 
     @Test
diff --git a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
index 73ddbe8c..ffec68a 100644
--- a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
@@ -51,6 +51,7 @@
 
 import com.android.server.EventLogTags;
 import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupWakeLock;
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
@@ -103,7 +104,7 @@
     @Mock private OperationStorage mOperationStorage;
     private ShadowLooper mShadowBackupLooper;
     private ShadowApplication mShadowApplication;
-    private UserBackupManagerService.BackupWakeLock mWakeLock;
+    private BackupWakeLock mWakeLock;
     private TransportData mTransport;
     private RestoreSet mRestoreSet1;
     private RestoreSet mRestoreSet2;
diff --git a/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java b/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
index 4d04c8b..10d23dc 100644
--- a/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
+++ b/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
@@ -39,6 +39,7 @@
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupManagerConstants;
 import com.android.server.backup.BackupManagerService;
+import com.android.server.backup.BackupWakeLock;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
 
@@ -114,7 +115,7 @@
             TransportManager transportManager,
             PackageManager packageManager,
             Handler backupHandler,
-            UserBackupManagerService.BackupWakeLock wakeLock,
+            BackupWakeLock wakeLock,
             BackupAgentTimeoutParameters agentTimeoutParameters) {
 
         when(backupManagerService.getContext()).thenReturn(application);
@@ -123,7 +124,7 @@
         when(backupManagerService.getBackupHandler()).thenReturn(backupHandler);
         when(backupManagerService.getQueueLock()).thenReturn(new Object());
         when(backupManagerService.getActivityManager()).thenReturn(mock(IActivityManager.class));
-        when(backupManagerService.getWakelock()).thenReturn(wakeLock);
+        when(backupManagerService.getWakeLock()).thenReturn(wakeLock);
         when(backupManagerService.getAgentTimeoutParameters()).thenReturn(agentTimeoutParameters);
 
         AccessorMock backupEnabled = mockAccessor(false);
@@ -161,10 +162,12 @@
                 });
     }
 
-    public static UserBackupManagerService.BackupWakeLock createBackupWakeLock(
-            Application application) {
+    /**
+     * Creates a wakelock for testing.
+     */
+    public static BackupWakeLock createBackupWakeLock(Application application) {
         PowerManager powerManager = application.getSystemService(PowerManager.class);
-        return new UserBackupManagerService.BackupWakeLock(
+        return new BackupWakeLock(
                 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*"), 0,
                 new BackupManagerConstants(Handler.getMain(), application.getContentResolver()));
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java
index e618433..2b7a62a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java
@@ -35,6 +35,7 @@
 
 import com.android.server.backup.BackupAgentConnectionManager;
 import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.BackupWakeLock;
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
@@ -81,7 +82,7 @@
     @Mock
     TransportManager mTransportManager;
     @Mock
-    UserBackupManagerService.BackupWakeLock mWakeLock;
+    BackupWakeLock mWakeLock;
 
     private final List<String> mEligiblePackages = new ArrayList<>();
 
@@ -94,7 +95,7 @@
         when(mBackupManagerService.getPackageManager()).thenReturn(mPackageManager);
         when(mBackupManagerService.getQueueLock()).thenReturn("something!");
         when(mBackupManagerService.isEnabled()).thenReturn(true);
-        when(mBackupManagerService.getWakelock()).thenReturn(mWakeLock);
+        when(mBackupManagerService.getWakeLock()).thenReturn(mWakeLock);
         when(mBackupManagerService.isSetupComplete()).thenReturn(true);
         when(mBackupManagerService.getAgentTimeoutParameters()).thenReturn(
                 mBackupAgentTimeoutParameters);
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index 3cb2745..d925624 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -89,6 +89,7 @@
 import android.os.PowerSaveState;
 import android.os.UserHandle;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
@@ -119,6 +120,7 @@
 import com.android.server.power.batterysaver.BatterySaverController;
 import com.android.server.power.batterysaver.BatterySaverPolicy;
 import com.android.server.power.batterysaver.BatterySaverStateMachine;
+import com.android.server.power.feature.flags.Flags;
 import com.android.server.power.feature.PowerManagerFlags;
 import com.android.server.testutils.OffsettableClock;
 
@@ -3456,6 +3458,25 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
+    public void testAcquireWakelock_screenTimeoutListenersDisabled_noCrashes() {
+        IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
+        when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds);
+
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP;
+        when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY))
+                .thenReturn(displayInfo);
+
+        createService();
+        startSystem();
+
+        acquireWakeLock("screenBright", PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
+                    Display.DEFAULT_DISPLAY);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
     public void testAddWakeLockKeepingScreenOn_addsToNotifierAndReportsTimeoutPolicyChange() {
         IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
         when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds);
@@ -3483,6 +3504,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
     public void test_addAndRemoveScreenTimeoutListener_propagatesToNotifier()
             throws Exception {
         IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
@@ -3509,6 +3531,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
     public void test_displayIsRemoved_clearsScreenTimeoutListeners()
             throws Exception {
         IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
@@ -3531,7 +3554,8 @@
     }
 
     @Test
-    public void testScreenWakeLockListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier()
+    @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API)
+    public void testScreenTimeoutListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier()
             throws Exception {
         IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP});
         when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds);
diff --git a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
index 115cdf6..e654b40 100644
--- a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java
@@ -37,11 +37,13 @@
 import android.os.IPowerStatsService;
 import android.os.Looper;
 import android.os.PowerMonitor;
+import android.os.PowerMonitorReadings;
 import android.os.ResultReceiver;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfigInterface;
+import android.util.IntArray;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -129,6 +131,8 @@
         }
     }
 
+    private final IntArray mFinePowerMonitorsPermissionGranted = new IntArray();
+
     private final PowerStatsService.Injector mInjector = new PowerStatsService.Injector() {
 
         @Override
@@ -220,6 +224,11 @@
         IntervalRandomNoiseGenerator createIntervalRandomNoiseGenerator() {
             return mMockNoiseGenerator;
         }
+
+        @Override
+        boolean checkFinePowerMonitorsPermission(Context context, int callingUid) {
+            return mFinePowerMonitorsPermissionGranted.contains(callingUid);
+        }
     };
 
     public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper {
@@ -1109,6 +1118,8 @@
         public int resultCode;
         public long[] energyUws;
         public long[] timestamps;
+        @PowerMonitorReadings.PowerMonitorGranularity
+        public int granularity;
 
         GetPowerMonitorsResult() {
             super(null);
@@ -1120,12 +1131,23 @@
             if (resultData != null) {
                 energyUws = resultData.getLongArray(IPowerStatsService.KEY_ENERGY);
                 timestamps = resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS);
+                granularity = resultData.getInt(IPowerStatsService.KEY_GRANULARITY);
             }
         }
     }
 
     @Test
     public void getPowerMonitors() {
+        testGetPowerMonitors(PowerMonitorReadings.GRANULARITY_UNSPECIFIED);
+    }
+
+    @Test
+    public void getPowerMonitors_finePowerMonitorPermissionGranted() {
+        mFinePowerMonitorsPermissionGranted.add(APP_UID);
+        testGetPowerMonitors(PowerMonitorReadings.GRANULARITY_FINE);
+    }
+
+    private void testGetPowerMonitors(int expectedGranularity) {
         mMockClock.realtime = 10 * 60_000;
         mMockNoiseGenerator.reseed(314);
 
@@ -1161,6 +1183,7 @@
 
         assertThat(result.energyUws).isEqualTo(new long[]{42, 142, 314, 514});
         assertThat(result.timestamps).isEqualTo(new long[]{600_000, 600_100, 600_000, 600_200});
+        assertThat(result.granularity).isEqualTo(expectedGranularity);
 
         // Test caching/throttling
         mMockClock.realtime += 1;
@@ -1180,6 +1203,7 @@
 
         assertThat(result.energyUws).isEqualTo(new long[]{42, 314});
         assertThat(result.timestamps).isEqualTo(new long[]{600_000, 600_000});
+        assertThat(result.granularity).isEqualTo(expectedGranularity);
 
         mMockClock.realtime += 10 * 60000;
 
@@ -1189,6 +1213,7 @@
         // This time, random noise is added
         assertThat(result.energyUws).isEqualTo(new long[]{298, 399});
         assertThat(result.timestamps).isEqualTo(new long[]{600_301, 600_401});
+        assertThat(result.granularity).isEqualTo(expectedGranularity);
     }
 
     @Test
@@ -1234,7 +1259,6 @@
         assertThrows(NullPointerException.class, () -> iPowerStatsService.getPowerMonitorReadings(
                 new int[] {0}, null));
     }
-
     @Test
     public void getEnergyConsumedAsync_halException() {
         mPowerStatsHALWrapper.exception = new IllegalArgumentException();
diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java
new file mode 100644
index 0000000..23c13bd
--- /dev/null
+++ b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java
@@ -0,0 +1,527 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.timezonedetector;
+
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.AUTO_REVERT_THRESHOLD;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_NONE;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_UNKNOWN;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_REJECTED;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_SUPERSEDED;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNKNOWN;
+import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNTRACKED;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.spy;
+
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.flags.Flags;
+import com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.TimeZoneChangeRecord;
+import com.android.server.timezonedetector.TimeZoneChangeListener.TimeZoneChangeEvent;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.time.InstantSource;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * White-box unit tests for {@link NotifyingTimeZoneChangeListener}.
+ */
+@RunWith(JUnitParamsRunner.class)
+@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS)
+public class NotifyingTimeZoneChangeListenerTest {
+
+    @ClassRule
+    public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+
+    @Rule(order = 0)
+    public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
+    @Rule(order = 1)
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    public static List<@TimeZoneDetectorStrategy.Origin Integer> getDetectionOrigins() {
+        return List.of(ORIGIN_LOCATION, ORIGIN_TELEPHONY);
+    }
+
+    private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234;
+    /** A time zone used for initialization that does not occur elsewhere in tests. */
+    private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
+    private static final String INTERACT_ACROSS_USERS_FULL_PERMISSION =
+            "android.permission.INTERACT_ACROSS_USERS_FULL";
+
+    @Mock
+    private Context mContext;
+    private UiAutomation mUiAutomation;
+
+    private FakeNotificationManager mNotificationManager;
+    private HandlerThread mHandlerThread;
+    private TestHandler mHandler;
+    private FakeServiceConfigAccessor mServiceConfigAccessor;
+    private int mUid;
+
+    private NotifyingTimeZoneChangeListener mTimeZoneChangeTracker;
+
+    @Before
+    public void setUp() {
+        mUid = Process.myUid();
+        // Create a thread + handler for processing the work that the service posts.
+        mHandlerThread = new HandlerThread("TimeZoneDetectorInternalTest");
+        mHandlerThread.start();
+        mHandler = new TestHandler(mHandlerThread.getLooper());
+
+        ConfigurationInternal config = new ConfigurationInternal.Builder()
+                .setUserId(mUid)
+                .setTelephonyDetectionFeatureSupported(true)
+                .setGeoDetectionFeatureSupported(true)
+                .setTelephonyFallbackSupported(false)
+                .setGeoDetectionRunInBackgroundEnabled(false)
+                .setEnhancedMetricsCollectionEnabled(false)
+                .setUserConfigAllowed(true)
+                .setAutoDetectionEnabledSetting(false)
+                .setLocationEnabledSetting(true)
+                .setGeoDetectionEnabledSetting(false)
+                .setNotificationsSupported(true)
+                .setNotificationsTrackingSupported(true)
+                .setNotificationsEnabledSetting(false)
+                .setManualChangeTrackingSupported(false)
+                .build();
+
+        mServiceConfigAccessor = spy(new FakeServiceConfigAccessor());
+        mServiceConfigAccessor.initializeCurrentUserConfiguration(config);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL_PERMISSION);
+
+        mNotificationManager = new FakeNotificationManager(mContext, InstantSource.system());
+
+        mTimeZoneChangeTracker = new NotifyingTimeZoneChangeListener(mHandler, mContext,
+                mServiceConfigAccessor, mNotificationManager);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+        mHandlerThread.join();
+    }
+
+    @Test
+    public void process_autoDetectionOff_noManualTracking_shouldTrackWithoutNotifying() {
+        enableTimeZoneNotifications();
+
+        TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+                /* id= */ 1,
+                new TimeZoneChangeEvent(
+                        /* elapsedRealtimeMillis= */ 0,
+                        /* unixEpochTimeMillis= */ 1726597800000L,
+                        /* origin= */ ORIGIN_MANUAL,
+                        /* userId= */ mUid,
+                        /* oldZoneId= */ "Europe/Paris",
+                        /* newZoneId= */ "Europe/London",
+                        /* newConfidence= */ 1,
+                        /* cause= */ "NO_REASON"));
+        expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+
+        assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+        mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+
+        assertEquals(expectedChangeEvent, mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+        assertEquals(0, mNotificationManager.getNotifications().size());
+        mHandler.assertTotalMessagesEnqueued(0);
+    }
+
+    @Test
+    public void process_autoDetectionOff_shouldTrackWithoutNotifying() {
+        enableNotificationsWithManualChangeTracking();
+
+        TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+                /* id= */ 1,
+                new TimeZoneChangeEvent(
+                        /* elapsedRealtimeMillis= */ 0,
+                        /* unixEpochTimeMillis= */ 1726597800000L,
+                        /* origin= */ ORIGIN_MANUAL,
+                        /* userId= */ mUid,
+                        /* oldZoneId= */ "Europe/Paris",
+                        /* newZoneId= */ "Europe/London",
+                        /* newConfidence= */ 1,
+                        /* cause= */ "NO_REASON"));
+        expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+
+        assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+        mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+
+        assertEquals(expectedChangeEvent, mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+        assertEquals(0, mNotificationManager.getNotifications().size());
+        mHandler.assertTotalMessagesEnqueued(1);
+    }
+
+    @Test
+    @Parameters(method = "getDetectionOrigins")
+    public void process_automaticDetection_trackingSupported(
+            @TimeZoneDetectorStrategy.Origin int origin) {
+        if (origin == ORIGIN_LOCATION) {
+            enableLocationTimeZoneDetection();
+        } else if (origin == ORIGIN_TELEPHONY) {
+            enableTelephonyTimeZoneDetection();
+        } else {
+            throw new IllegalStateException(
+                    "The given origin has not been implemented for this test: " + origin);
+        }
+
+        enableNotificationsWithManualChangeTracking();
+
+        TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+                /* id= */ 1,
+                new TimeZoneChangeEvent(
+                        /* elapsedRealtimeMillis= */ 0,
+                        /* unixEpochTimeMillis= */ 1726597800000L,
+                        /* origin= */ origin,
+                        /* userId= */ mUid,
+                        /* oldZoneId= */ "Europe/Paris",
+                        /* newZoneId= */ "Europe/London",
+                        /* newConfidence= */ 1,
+                        /* cause= */ "NO_REASON"));
+        expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+        assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+        // lastTrackedChangeEvent == null
+        mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+        TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+        assertEquals(expectedChangeEvent, trackedEvent1);
+        assertEquals(1, mNotificationManager.getNotifications().size());
+        mHandler.assertTotalMessagesEnqueued(1);
+
+        expectedChangeEvent = new TimeZoneChangeRecord(
+                /* id= */ 2,
+                new TimeZoneChangeEvent(
+                        /* elapsedRealtimeMillis= */ 1000L,
+                        /* unixEpochTimeMillis= */ 1726597800000L + 1000L,
+                        /* origin= */ origin,
+                        /* userId= */ mUid,
+                        /* oldZoneId= */ "Europe/London",
+                        /* newZoneId= */ "Europe/Paris",
+                        /* newConfidence= */ 1,
+                        /* cause= */ "NO_REASON"));
+        expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+        // lastTrackedChangeEvent != null
+        mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+        TimeZoneChangeRecord trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+        assertEquals(STATUS_SUPERSEDED, trackedEvent1.getStatus());
+        assertEquals(expectedChangeEvent, trackedEvent2);
+        assertEquals(2, mNotificationManager.getNotifications().size());
+        mHandler.assertTotalMessagesEnqueued(2);
+
+        disableTimeZoneAutoDetection();
+
+        // Test manual change within revert threshold
+        {
+            expectedChangeEvent = new TimeZoneChangeRecord(
+                    /* id= */ 3,
+                    new TimeZoneChangeEvent(
+                            /* elapsedRealtimeMillis= */ 999L + AUTO_REVERT_THRESHOLD,
+                            /* unixEpochTimeMillis= */
+                            1726597800000L + 999L + AUTO_REVERT_THRESHOLD,
+                            /* origin= */ ORIGIN_MANUAL,
+                            /* userId= */ mUid,
+                            /* oldZoneId= */ "Europe/Paris",
+                            /* newZoneId= */ "Europe/London",
+                            /* newConfidence= */ 1,
+                            /* cause= */ "NO_REASON"));
+            expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+
+            mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+            TimeZoneChangeRecord trackedEvent3 =
+                    mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+            // The user manually changed the time zone within a short period of receiving the
+            // notification, indicating that they rejected the automatic change of time zone
+            assertEquals(STATUS_REJECTED, trackedEvent2.getStatus());
+            assertEquals(expectedChangeEvent, trackedEvent3);
+            assertEquals(2, mNotificationManager.getNotifications().size());
+            mHandler.assertTotalMessagesEnqueued(3);
+        }
+
+        // Test manual change outside of revert threshold
+        {
+            // [START] Reset previous event
+            enableNotificationsWithManualChangeTracking();
+            mTimeZoneChangeTracker.process(trackedEvent2.getEvent());
+            trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+            disableTimeZoneAutoDetection();
+            // [END] Reset previous event
+
+            expectedChangeEvent = new TimeZoneChangeRecord(
+                    /* id= */ 5,
+                    new TimeZoneChangeEvent(
+                            /* elapsedRealtimeMillis= */ 1001L + AUTO_REVERT_THRESHOLD,
+                            /* unixEpochTimeMillis= */
+                            1726597800000L + 1001L + AUTO_REVERT_THRESHOLD,
+                            /* origin= */ ORIGIN_MANUAL,
+                            /* userId= */ mUid,
+                            /* oldZoneId= */ "Europe/Paris",
+                            /* newZoneId= */ "Europe/London",
+                            /* newConfidence= */ 1,
+                            /* cause= */ "NO_REASON"));
+            expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE);
+
+            mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+            TimeZoneChangeRecord trackedEvent3 =
+                    mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+            // The user manually changed the time zone outside of the period we consider as a revert
+            assertEquals(STATUS_SUPERSEDED, trackedEvent2.getStatus());
+            assertEquals(expectedChangeEvent, trackedEvent3);
+            assertEquals(3, mNotificationManager.getNotifications().size());
+            mHandler.assertTotalMessagesEnqueued(5);
+        }
+    }
+
+    @Test
+    @Parameters(method = "getDetectionOrigins")
+    public void process_automaticDetection_trackingSupported_missingTransition(
+            @TimeZoneDetectorStrategy.Origin int origin) {
+        if (origin == ORIGIN_LOCATION) {
+            enableLocationTimeZoneDetection();
+        } else if (origin == ORIGIN_TELEPHONY) {
+            enableTelephonyTimeZoneDetection();
+        } else {
+            throw new IllegalStateException(
+                    "The given origin has not been implemented for this test: " + origin);
+        }
+
+        enableNotificationsWithManualChangeTracking();
+
+        TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+                /* id= */ 1,
+                new TimeZoneChangeEvent(
+                        /* elapsedRealtimeMillis= */ 0,
+                        /* unixEpochTimeMillis= */ 1726597800000L,
+                        /* origin= */ origin,
+                        /* userId= */ mUid,
+                        /* oldZoneId= */ "Europe/Paris",
+                        /* newZoneId= */ "Europe/London",
+                        /* newConfidence= */ 1,
+                        /* cause= */ "NO_REASON"));
+        expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+        assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+        // lastTrackedChangeEvent == null
+        mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+        TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+        assertEquals(expectedChangeEvent, trackedEvent1);
+        assertEquals(1, mNotificationManager.getNotifications().size());
+        mHandler.assertTotalMessagesEnqueued(1);
+
+        expectedChangeEvent = new TimeZoneChangeRecord(
+                /* id= */ 3,
+                new TimeZoneChangeEvent(
+                        /* elapsedRealtimeMillis= */ 1000L,
+                        /* unixEpochTimeMillis= */ 1726597800000L + 1000L,
+                        /* origin= */ origin,
+                        /* userId= */ mUid,
+                        /* oldZoneId= */ "Europe/Athens",
+                        /* newZoneId= */ "Europe/Paris",
+                        /* newConfidence= */ 1,
+                        /* cause= */ "NO_REASON"));
+        expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+        // lastTrackedChangeEvent != null
+        mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+        TimeZoneChangeRecord trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+        assertEquals(STATUS_SUPERSEDED, trackedEvent1.getStatus());
+        assertEquals(expectedChangeEvent, trackedEvent2);
+        assertEquals(2, mNotificationManager.getNotifications().size());
+        mHandler.assertTotalMessagesEnqueued(2);
+    }
+
+    @Test
+    @Parameters(method = "getDetectionOrigins")
+    public void process_automaticDetection_trackingSupported_sameOffset(
+            @TimeZoneDetectorStrategy.Origin int origin) {
+        if (origin == ORIGIN_LOCATION) {
+            enableLocationTimeZoneDetection();
+        } else if (origin == ORIGIN_TELEPHONY) {
+            enableTelephonyTimeZoneDetection();
+        } else {
+            throw new IllegalStateException(
+                    "The given origin has not been implemented for this test: " + origin);
+        }
+
+        enableNotificationsWithManualChangeTracking();
+
+        TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord(
+                /* id= */ 1,
+                new TimeZoneChangeEvent(
+                        /* elapsedRealtimeMillis= */ 0,
+                        /* unixEpochTimeMillis= */ 1726597800000L,
+                        /* origin= */ origin,
+                        /* userId= */ mUid,
+                        /* oldZoneId= */ "Europe/Paris",
+                        /* newZoneId= */ "Europe/Rome",
+                        /* newConfidence= */ 1,
+                        /* cause= */ "NO_REASON"));
+        expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN);
+
+        assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord());
+
+        // lastTrackedChangeEvent == null
+        mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent());
+        TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord();
+
+        assertEquals(expectedChangeEvent, trackedEvent1);
+        // No notification sent for the same UTC offset
+        assertEquals(0, mNotificationManager.getNotifications().size());
+        mHandler.assertTotalMessagesEnqueued(1);
+    }
+
+    private void enableLocationTimeZoneDetection() {
+        ConfigurationInternal oldConfiguration =
+                mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+        ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+                .setAutoDetectionEnabledSetting(true)
+                .setGeoDetectionFeatureSupported(true)
+                .setGeoDetectionEnabledSetting(true)
+                .build();
+
+        mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+    }
+
+    private void enableTelephonyTimeZoneDetection() {
+        ConfigurationInternal oldConfiguration =
+                mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+        ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+                .setAutoDetectionEnabledSetting(true)
+                .setGeoDetectionEnabledSetting(false)
+                .setTelephonyDetectionFeatureSupported(true)
+                .setTelephonyFallbackSupported(true)
+                .build();
+
+        mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+    }
+
+    private void enableTimeZoneNotifications() {
+        ConfigurationInternal oldConfiguration =
+                mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+        ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+                .setNotificationsSupported(true)
+                .setNotificationsTrackingSupported(true)
+                .setNotificationsEnabledSetting(true)
+                .setManualChangeTrackingSupported(false)
+                .build();
+
+        mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+    }
+
+    private void enableNotificationsWithManualChangeTracking() {
+        ConfigurationInternal oldConfiguration =
+                mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+        ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+                .setNotificationsSupported(true)
+                .setNotificationsTrackingSupported(true)
+                .setNotificationsEnabledSetting(true)
+                .setManualChangeTrackingSupported(true)
+                .build();
+
+        mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+    }
+
+    private void disableTimeZoneAutoDetection() {
+        ConfigurationInternal oldConfiguration =
+                mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+        ConfigurationInternal newConfiguration = toBuilder(oldConfiguration)
+                .setAutoDetectionEnabledSetting(false)
+                .setGeoDetectionEnabledSetting(false)
+                .build();
+
+        mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration);
+    }
+
+    private ConfigurationInternal.Builder toBuilder(ConfigurationInternal config) {
+        return new ConfigurationInternal.Builder()
+                .setUserId(config.getUserId())
+                .setTelephonyDetectionFeatureSupported(config.isTelephonyDetectionSupported())
+                .setGeoDetectionFeatureSupported(config.isGeoDetectionSupported())
+                .setTelephonyFallbackSupported(config.isTelephonyFallbackSupported())
+                .setGeoDetectionRunInBackgroundEnabled(
+                        config.getGeoDetectionRunInBackgroundEnabledSetting())
+                .setEnhancedMetricsCollectionEnabled(config.isEnhancedMetricsCollectionEnabled())
+                .setUserConfigAllowed(config.isUserConfigAllowed())
+                .setAutoDetectionEnabledSetting(config.getAutoDetectionEnabledSetting())
+                .setLocationEnabledSetting(config.getLocationEnabledSetting())
+                .setGeoDetectionEnabledSetting(config.getGeoDetectionEnabledSetting())
+                .setNotificationsTrackingSupported(config.isNotificationTrackingSupported())
+                .setNotificationsEnabledSetting(config.getNotificationsEnabledBehavior())
+                .setNotificationsSupported(config.areNotificationsSupported())
+                .setManualChangeTrackingSupported(config.isManualChangeTrackingSupported());
+    }
+
+    private static class FakeNotificationManager extends NotificationManager {
+
+        private final List<Notification> mNotifications = new ArrayList<>();
+
+        FakeNotificationManager(Context context, InstantSource clock) {
+            super(context, clock);
+        }
+
+        @Override
+        public void notifyAsUser(@Nullable String tag, int id, Notification notification,
+                UserHandle user) {
+            mNotifications.add(notification);
+        }
+
+        public List<Notification> getNotifications() {
+            return mNotifications;
+        }
+    }
+}
diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
index 47a9b2c..9a01fa2 100644
--- a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
+++ b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
@@ -41,6 +41,9 @@
 import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_ENABLED;
 import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_USER_RESTRICTED_AUTO_ENABLED;
 import static com.android.server.timezonedetector.ConfigInternalForTests.USER_ID;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY;
 import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH;
 import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST;
 import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW;
@@ -73,16 +76,21 @@
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.timezone.TimeZoneProviderStatus;
 import android.util.IndentingPrintWriter;
 
 import com.android.server.SystemTimeZone.TimeZoneConfidence;
+import com.android.server.flags.Flags;
 import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion;
 
 import junitparams.JUnitParamsRunner;
 import junitparams.Parameters;
 
 import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -98,8 +106,14 @@
  * White-box unit tests for {@link TimeZoneDetectorStrategyImpl}.
  */
 @RunWith(JUnitParamsRunner.class)
+@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS)
 public class TimeZoneDetectorStrategyImplTest {
 
+    @ClassRule
+    public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
     private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234;
     /** A time zone used for initialization that does not occur elsewhere in tests. */
     private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
@@ -130,6 +144,7 @@
 
     private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy;
     private FakeEnvironment mFakeEnvironment;
+    private FakeTimeZoneChangeEventListener mFakeTimeZoneChangeEventTracker;
 
     private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy;
 
@@ -139,9 +154,10 @@
         mFakeServiceConfigAccessorSpy = spy(new FakeServiceConfigAccessor());
         mFakeServiceConfigAccessorSpy.initializeCurrentUserConfiguration(
                 CONFIG_AUTO_DISABLED_GEO_DISABLED);
+        mFakeTimeZoneChangeEventTracker = new FakeTimeZoneChangeEventListener();
 
         mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl(
-                mFakeServiceConfigAccessorSpy, mFakeEnvironment);
+                mFakeServiceConfigAccessorSpy, mFakeEnvironment, mFakeTimeZoneChangeEventTracker);
     }
 
     @Test
@@ -363,6 +379,10 @@
         // SlotIndex1 should always beat slotIndex2, all other things being equal.
         assertEquals(expectedSlotIndex1ScoredSuggestion,
                 mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
+
+        if (Flags.datetimeNotifications()) {
+            assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+        }
     }
 
     /**
@@ -398,6 +418,10 @@
                     SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
+
+            if (Flags.datetimeNotifications()) {
+                assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+            }
         }
 
         // A good quality suggestion will be used.
@@ -415,6 +439,13 @@
                     SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
+
+            if (Flags.datetimeNotifications()) {
+                assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+                assertEquals(ORIGIN_TELEPHONY,
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst()
+                                .getOrigin());
+            }
         }
 
         // A low quality suggestion will be accepted, but not used to set the device time zone.
@@ -432,6 +463,11 @@
                     SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
+
+            if (Flags.datetimeNotifications()) {
+                // Still 1 from last good quality suggestion but not recorded as quality is too low
+                assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+            }
         }
     }
 
@@ -492,6 +528,17 @@
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
         }
+
+        if (Flags.datetimeNotifications()) {
+            /*
+             * Only 6 out of 7 tests have a quality good enough to trigger an event and the
+             * configuration is reset at every loop.
+             */
+            assertEquals(6, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+            assertEquals(ORIGIN_TELEPHONY,
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst()
+                            .getOrigin());
+        }
     }
 
     @Test
@@ -518,6 +565,18 @@
         for (TelephonyTestCase testCase : descendingCasesByScore) {
             makeSlotIndex1SuggestionAndCheckState(script, testCase);
         }
+
+        if (Flags.datetimeNotifications()) {
+            /*
+             * Only 6 out of 7 tests have a quality good enough to trigger an event and the
+             * set of tests is run twice.
+             */
+            List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+            assertEquals(12, timeZoneChangeEvents.size());
+            assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin());
+        }
     }
 
     private void makeSlotIndex1SuggestionAndCheckState(Script script, TelephonyTestCase testCase) {
@@ -641,6 +700,18 @@
                     .verifyLatestQualifiedTelephonySuggestionReceived(
                             SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion);
         }
+
+        if (Flags.datetimeNotifications()) {
+            /*
+             * Only 6 out of 7 tests have a quality good enough to trigger an event and the
+             * simulation runs twice per loop with a different time zone (i.e. London and Paris).
+             */
+            List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+            assertEquals(12, timeZoneChangeEvents.size());
+            assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin());
+        }
     }
 
     /**
@@ -683,6 +754,14 @@
         // Latest suggestion should be used.
         script.simulateSetAutoMode(true)
                 .verifyTimeZoneChangedAndReset(newYorkSuggestion);
+
+        if (Flags.datetimeNotifications()) {
+            List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+            assertEquals(2, timeZoneChangeEvents.size());
+            assertTrue(timeZoneChangeEvents.stream()
+                    .allMatch(x -> x.getOrigin() == ORIGIN_TELEPHONY));
+        }
     }
 
     @Test
@@ -714,6 +793,10 @@
                 .verifyTimeZoneNotChanged();
 
         assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
+
+        if (Flags.datetimeNotifications()) {
+            assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+        }
     }
 
     @Test
@@ -732,6 +815,15 @@
                 .verifyTimeZoneChangedAndReset(manualSuggestion);
 
         assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion());
+
+        if (Flags.datetimeNotifications()) {
+            List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+            assertEquals(1, timeZoneChangeEvents.size());
+            assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin()
+            );
+        }
     }
 
     @Test
@@ -754,6 +846,10 @@
                 .verifyTimeZoneNotChanged();
 
         assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
+
+        if (Flags.datetimeNotifications()) {
+            assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+        }
     }
 
     @Test
@@ -780,6 +876,16 @@
             script.verifyTimeZoneNotChanged();
             assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion());
         }
+
+        List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+        if (Flags.datetimeNotifications() && expectedResult) {
+            assertEquals(1, timeZoneChangeEvents.size());
+            assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin());
+        } else {
+            assertEmpty(timeZoneChangeEvents);
+        }
     }
 
     @Test
@@ -830,6 +936,10 @@
             // Assert internal service state.
             script.verifyCachedDetectorStatus(expectedDetectorStatus)
                     .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+            if (Flags.datetimeNotifications()) {
+                assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+            }
         }
 
         {
@@ -857,6 +967,10 @@
             // Assert internal service state.
             script.verifyCachedDetectorStatus(expectedDetectorStatus)
                     .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+            if (Flags.datetimeNotifications()) {
+                assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+            }
         }
     }
 
@@ -893,6 +1007,10 @@
         // Assert internal service state.
         script.verifyCachedDetectorStatus(expectedDetectorStatus)
                 .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+        if (Flags.datetimeNotifications()) {
+            assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+        }
     }
 
     @Test
@@ -927,6 +1045,10 @@
         // Assert internal service state.
         script.verifyCachedDetectorStatus(expectedDetectorStatus)
                 .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+        if (Flags.datetimeNotifications()) {
+            assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+        }
     }
 
     @Test
@@ -962,6 +1084,14 @@
         // Assert internal service state.
         script.verifyCachedDetectorStatus(expectedDetectorStatus)
                 .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+        if (Flags.datetimeNotifications()) {
+            List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+            assertEquals(1, timeZoneChangeEvents.size());
+            assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.getFirst().getOrigin());
+        }
     }
 
     /**
@@ -999,6 +1129,17 @@
         script.simulateLocationAlgorithmEvent(londonOrParisEvent)
                 .verifyTimeZoneNotChanged()
                 .verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent);
+
+        if (Flags.datetimeNotifications()) {
+            // we do not record events if the time zone does not change (i.e. 2 / 4 of the
+            // simulated cases)
+            List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+            assertEquals(2, timeZoneChangeEvents.size());
+            assertTrue(timeZoneChangeEvents.stream()
+                    .allMatch(x -> x.getOrigin() == ORIGIN_LOCATION));
+        }
     }
 
     /**
@@ -1059,6 +1200,16 @@
 
         // A configuration change is considered a "state change".
         assertStateChangeNotificationsSent(stateChangeListener, 1);
+
+        if (Flags.datetimeNotifications()) {
+            List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+            assertEquals(3, timeZoneChangeEvents.size());
+            assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin());
+            assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin());
+            assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(2).getOrigin());
+        }
     }
 
     @Test
@@ -1088,6 +1239,14 @@
                     .simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
                     .verifyTimeZoneChangedAndReset(telephonySuggestion)
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+                assertEquals(1, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin());
+            }
         }
 
         // Receiving an "uncertain" geolocation suggestion should have no effect.
@@ -1098,6 +1257,11 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                // unchanged
+                assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+            }
         }
 
         // Receiving a "certain" geolocation suggestion should disable telephony fallback mode.
@@ -1109,6 +1273,14 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+                assertEquals(2, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin());
+            }
         }
 
         // Used to record the last telephony suggestion received, which will be used when fallback
@@ -1125,6 +1297,11 @@
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(false);
             lastTelephonySuggestion = telephonySuggestion;
+
+            if (Flags.datetimeNotifications()) {
+                // unchanged
+                assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+            }
         }
 
         // Geolocation suggestions should continue to be used as normal (previous telephony
@@ -1151,6 +1328,14 @@
                     // No change needed, device will already be set to Europe/Rome.
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(false);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+                assertEquals(3, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin());
+            }
         }
 
         // Enable telephony fallback. Nothing will change, because the geolocation is still certain,
@@ -1160,6 +1345,11 @@
                     .simulateEnableTelephonyFallback()
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                // unchanged
+                assertEquals(3, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+            }
         }
 
         // Make the geolocation algorithm uncertain.
@@ -1170,6 +1360,14 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+                assertEquals(4, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin());
+            }
         }
 
         // Make the geolocation algorithm certain, disabling telephony fallback.
@@ -1181,6 +1379,14 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+                assertEquals(5, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin());
+            }
         }
 
         // Demonstrate what happens when geolocation is uncertain when telephony fallback is
@@ -1195,6 +1401,14 @@
                     .simulateEnableTelephonyFallback()
                     .verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+                assertEquals(6, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(5).getOrigin());
+            }
         }
     }
 
@@ -1225,6 +1439,13 @@
                     .simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
                     .verifyTimeZoneChangedAndReset(telephonySuggestion)
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+                assertEquals(1, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin());
+            }
         }
 
         // Receiving an "uncertain" geolocation suggestion without a status should have no effect.
@@ -1235,6 +1456,11 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                // unchanged
+                assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+            }
         }
 
         // Receiving a "certain" geolocation suggestion should disable telephony fallback mode.
@@ -1246,6 +1472,13 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+                assertEquals(2, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin());
+            }
         }
 
         // Used to record the last telephony suggestion received, which will be used when fallback
@@ -1262,6 +1495,11 @@
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(false);
             lastTelephonySuggestion = telephonySuggestion;
+
+            if (Flags.datetimeNotifications()) {
+                // unchanged
+                assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size());
+            }
         }
 
         // Geolocation suggestions should continue to be used as normal (previous telephony
@@ -1291,6 +1529,13 @@
                     // No change needed, device will already be set to Europe/Rome.
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(false);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+                assertEquals(3, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin());
+            }
         }
 
         // Enable telephony fallback via a LocationAlgorithmEvent containing an "uncertain"
@@ -1310,6 +1555,13 @@
             script.simulateLocationAlgorithmEvent(uncertainEventBlockedBySettings)
                     .verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+                assertEquals(4, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin());
+            }
         }
 
         // Make the geolocation algorithm certain, disabling telephony fallback.
@@ -1321,6 +1573,13 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+                assertEquals(5, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin());
+            }
         }
     }
 
@@ -1349,6 +1608,10 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+            }
         }
 
         // Make an uncertain geolocation suggestion, there is no telephony suggestion to fall back
@@ -1360,6 +1623,10 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents());
+            }
         }
 
         // Similar to the case above, but force a fallback attempt after making a "certain"
@@ -1386,6 +1653,13 @@
                     .simulateEnableTelephonyFallback()
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
+
+            if (Flags.datetimeNotifications()) {
+                List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                        mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+                assertEquals(1, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(0).getOrigin());
+            }
         }
     }
 
@@ -1803,6 +2077,16 @@
                     userId, manualTimeZoneSuggestion, bypassUserPolicyChecks);
             assertEquals(expectedResult, actualResult);
 
+            List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents =
+                    mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents();
+
+            if (actualResult && Flags.datetimeNotifications()) {
+                assertEquals(1, timeZoneChangeEvents.size());
+                assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin());
+            } else {
+                assertEmpty(timeZoneChangeEvents);
+            }
+
             return this;
         }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 5ed2df3..cc447a1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -1269,6 +1269,7 @@
         final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
         spyOn(container);
         spyOn(surfaceAnimator);
+        doReturn(t).when(container).getSyncTransaction();
 
         // Trigger for first relative layer call.
         container.assignRelativeLayer(t, relativeParent, 1 /* layer */);
@@ -1295,6 +1296,7 @@
         spyOn(container);
         spyOn(surfaceAnimator);
         spyOn(surfaceFreezer);
+        doReturn(t).when(container).getSyncTransaction();
 
         container.setLayer(t, 1);
         container.setRelativeLayer(t, relativeParent, 2);