Merge changes from topic "caitlinshk-callchipflag-removal" into main

* changes:
  [SB] Remove status_bar_call_chip_notification_icon flag.
  [SB] Remove status_bar_use_repos_for_call_chip flag.
  [SB][Notif] Only hide text in chip that was actually tapped.
  [SB][Notif] Re-tapping notif chip hides the HUN.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 1f3e655..717a2ac 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -105,7 +105,6 @@
 import android.content.res.AssetManager;
 import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
-import android.content.res.ResourceTimer;
 import android.content.res.Resources;
 import android.content.res.ResourcesImpl;
 import android.content.res.loader.ResourcesLoader;
@@ -5254,7 +5253,6 @@
 
             Resources.dumpHistory(pw, "");
             pw.flush();
-            ResourceTimer.dumpTimers(info.fd.getFileDescriptor(), "-refresh");
             if (info.finishCallback != null) {
                 info.finishCallback.sendResult(null);
             }
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 67f7bee..b5ac4e7 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -70,7 +70,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.nio.charset.Charset;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -1064,7 +1063,7 @@
             Log.e(TAG, "Save lock exception", e);
             success = false;
         } finally {
-            Arrays.fill(password, (byte) 0);
+            LockPatternUtils.zeroize(password);
         }
         return success;
     }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 5176aee..252978f 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1968,6 +1968,13 @@
         @SystemApi
         public static final int SEMANTIC_ACTION_CONVERSATION_IS_PHISHING = 12;
 
+        /**
+         * {@link #extras} key to a boolean defining if this action requires special visual
+         * treatment.
+         * @hide
+         */
+        public static final String EXTRA_IS_MAGIC = "android.extra.IS_MAGIC";
+
         private final Bundle mExtras;
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
         private Icon mIcon;
@@ -6207,7 +6214,7 @@
             int textColor = Colors.flattenAlpha(getPrimaryTextColor(p), pillColor);
             contentView.setInt(R.id.expand_button, "setDefaultTextColor", textColor);
             contentView.setInt(R.id.expand_button, "setDefaultPillColor", pillColor);
-            // Use different highlighted colors for e.g. unopened groups
+            // Use different highlighted colors for conversations' unread count
             if (p.mHighlightExpander) {
                 pillColor = Colors.flattenAlpha(
                         getColors(p).getTertiaryFixedDimAccentColor(), bgColor);
@@ -6807,8 +6814,6 @@
         public RemoteViews makeNotificationGroupHeader() {
             return makeNotificationHeader(mParams.reset()
                     .viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER)
-                    // Highlight group expander until the group is first opened
-                    .highlightExpander(Flags.notificationsRedesignTemplates())
                     .fillTextsFrom(this));
         }
 
@@ -6984,14 +6989,12 @@
          * @param useRegularSubtext uses the normal subtext set if there is one available. Otherwise
          *                          a new subtext is created consisting of the content of the
          *                          notification.
-         * @param highlightExpander whether the expander should use the highlighted colors
          * @hide
          */
-        public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext,
-                boolean highlightExpander) {
+        public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) {
             StandardTemplateParams p = mParams.reset()
                     .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED)
-                    .highlightExpander(highlightExpander)
+                    .highlightExpander(false)
                     .fillTextsFrom(this);
             if (!useRegularSubtext || TextUtils.isEmpty(p.mSubText)) {
                 p.summaryText(createSummaryText());
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 42c7441..311e24b 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -83,7 +83,6 @@
 public class VirtualDeviceInternal {
 
     private final Context mContext;
-    private final IVirtualDeviceManager mService;
     private final IVirtualDevice mVirtualDevice;
     private final Object mActivityListenersLock = new Object();
     @GuardedBy("mActivityListenersLock")
@@ -206,7 +205,6 @@
             Context context,
             int associationId,
             VirtualDeviceParams params) throws RemoteException {
-        mService = service;
         mContext = context.getApplicationContext();
         mVirtualDevice = service.createVirtualDevice(
                 new Binder(),
@@ -217,11 +215,7 @@
                 mSoundEffectListener);
     }
 
-    VirtualDeviceInternal(
-            IVirtualDeviceManager service,
-            Context context,
-            IVirtualDevice virtualDevice) {
-        mService = service;
+    VirtualDeviceInternal(Context context, IVirtualDevice virtualDevice) {
         mContext = context.getApplicationContext();
         mVirtualDevice = virtualDevice;
         try {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index ed2fd99..73ea9f0 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -577,9 +577,8 @@
         }
 
         /** @hide */
-        public VirtualDevice(IVirtualDeviceManager service, Context context,
-                IVirtualDevice virtualDevice) {
-            mVirtualDeviceInternal = new VirtualDeviceInternal(service, context, virtualDevice);
+        public VirtualDevice(Context context, IVirtualDevice virtualDevice) {
+            mVirtualDeviceInternal = new VirtualDeviceInternal(context, virtualDevice);
         }
 
         /**
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index f538e9f..0754578 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -25,7 +25,6 @@
 import android.ravenwood.annotation.RavenwoodClassLoadHook;
 import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 import android.text.TextUtils;
-import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
 
@@ -51,7 +50,6 @@
 @RavenwoodKeepWholeClass
 @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK)
 public final class ApkAssets {
-    private static final boolean DEBUG = false;
 
     /**
      * The apk assets contains framework resource values specified by the system.
@@ -136,17 +134,6 @@
     @Nullable
     private final AssetsProvider mAssets;
 
-    @NonNull
-    private String mName;
-
-    private static final int UPTODATE_FALSE = 0;
-    private static final int UPTODATE_TRUE = 1;
-    private static final int UPTODATE_ALWAYS_TRUE = 2;
-
-    // Start with the only value that may change later and would force a native call to
-    // double check it.
-    private int mPreviousUpToDateResult = UPTODATE_TRUE;
-
     /**
      * Creates a new ApkAssets instance from the given path on disk.
      *
@@ -317,7 +304,7 @@
 
     private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags,
             @Nullable AssetsProvider assets) throws IOException {
-        this(format, flags, assets, path);
+        this(format, flags, assets);
         Objects.requireNonNull(path, "path");
         mNativePtr = nativeLoad(format, path, flags, assets);
         mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
@@ -326,7 +313,7 @@
     private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd,
             @NonNull String friendlyName, @PropertyFlags int flags, @Nullable AssetsProvider assets)
             throws IOException {
-        this(format, flags, assets, friendlyName);
+        this(format, flags, assets);
         Objects.requireNonNull(fd, "fd");
         Objects.requireNonNull(friendlyName, "friendlyName");
         mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets);
@@ -336,7 +323,7 @@
     private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd,
             @NonNull String friendlyName, long offset, long length, @PropertyFlags int flags,
             @Nullable AssetsProvider assets) throws IOException {
-        this(format, flags, assets, friendlyName);
+        this(format, flags, assets);
         Objects.requireNonNull(fd, "fd");
         Objects.requireNonNull(friendlyName, "friendlyName");
         mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets);
@@ -344,17 +331,16 @@
     }
 
     private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) {
-        this(FORMAT_APK, flags, assets, "empty");
+        this(FORMAT_APK, flags, assets);
         mNativePtr = nativeLoadEmpty(flags, assets);
         mStringBlock = null;
     }
 
     private ApkAssets(@FormatType int format, @PropertyFlags int flags,
-            @Nullable AssetsProvider assets, @NonNull String name) {
+            @Nullable AssetsProvider assets) {
         mFlags = flags;
         mAssets = assets;
         mIsOverlay = format == FORMAT_IDMAP;
-        if (DEBUG) mName = name;
     }
 
     @UnsupportedAppUsage
@@ -435,41 +421,13 @@
         }
     }
 
-    private static double intervalMs(long beginNs, long endNs) {
-        return (endNs - beginNs) / 1000000.0;
-    }
-
     /**
      * Returns false if the underlying APK was changed since this ApkAssets was loaded.
      */
     public boolean isUpToDate() {
-        // This function is performance-critical - it's called multiple times on every Resources
-        // object creation, and on few other cache accesses - so it's important to avoid the native
-        // call when we know for sure what it will return (which is the case for both ALWAYS_TRUE
-        // and FALSE).
-        if (mPreviousUpToDateResult != UPTODATE_TRUE) {
-            return mPreviousUpToDateResult == UPTODATE_ALWAYS_TRUE;
-        }
-        final long beforeTs, afterLockTs, afterNativeTs, afterUnlockTs;
-        if (DEBUG) beforeTs = System.nanoTime();
-        final int res;
         synchronized (this) {
-            if (DEBUG) afterLockTs = System.nanoTime();
-            res = nativeIsUpToDate(mNativePtr);
-            if (DEBUG) afterNativeTs = System.nanoTime();
+            return nativeIsUpToDate(mNativePtr);
         }
-        if (DEBUG) {
-            afterUnlockTs = System.nanoTime();
-            if (afterUnlockTs - beforeTs >= 10L * 1000000) {
-                Log.d("ApkAssets", "isUpToDate(" + mName + ") took "
-                        + intervalMs(beforeTs, afterUnlockTs)
-                        + " ms: " + intervalMs(beforeTs, afterLockTs)
-                        + " / " + intervalMs(afterLockTs, afterNativeTs)
-                        + " / " + intervalMs(afterNativeTs, afterUnlockTs));
-            }
-        }
-        mPreviousUpToDateResult = res;
-        return res != UPTODATE_FALSE;
     }
 
     public boolean isSystem() {
@@ -529,7 +487,7 @@
     private static native @NonNull String nativeGetAssetPath(long ptr);
     private static native @NonNull String nativeGetDebugName(long ptr);
     private static native long nativeGetStringBlock(long ptr);
-    @CriticalNative private static native int nativeIsUpToDate(long ptr);
+    @CriticalNative private static native boolean nativeIsUpToDate(long ptr);
     private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException;
     private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr,
             String overlayableName) throws IOException;
diff --git a/core/java/android/content/res/ResourceTimer.java b/core/java/android/content/res/ResourceTimer.java
index 2d1bf4d..d51f64c 100644
--- a/core/java/android/content/res/ResourceTimer.java
+++ b/core/java/android/content/res/ResourceTimer.java
@@ -17,10 +17,13 @@
 package android.content.res;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 import android.app.AppProtoEnums;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.SystemClock;
 import android.text.TextUtils;
@@ -30,7 +33,6 @@
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.FrameworkStatsLog;
 
-import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -275,40 +277,38 @@
      * Update the metrics information and dump it.
      * @hide
      */
-    public static void dumpTimers(@NonNull FileDescriptor fd, String... args) {
-        try (PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd))) {
-            pw.println("\nDumping ResourceTimers");
-
-            final boolean enabled;
-            synchronized (sLock) {
-                enabled = sEnabled && sConfig != null;
-            }
-            if (!enabled) {
+    public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) {
+        FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor());
+        PrintWriter pw = new FastPrintWriter(fout);
+        synchronized (sLock) {
+            if (!sEnabled || (sConfig == null)) {
                 pw.println("  Timers are not enabled in this process");
+                pw.flush();
                 return;
             }
+        }
 
-            // Look for the --refresh switch.  If the switch is present, then sTimers is updated.
-            // Otherwise, the current value of sTimers is displayed.
-            boolean refresh = Arrays.asList(args).contains("-refresh");
+        // Look for the --refresh switch.  If the switch is present, then sTimers is updated.
+        // Otherwise, the current value of sTimers is displayed.
+        boolean refresh = Arrays.asList(args).contains("-refresh");
 
-            synchronized (sLock) {
-                update(refresh);
-                long runtime = sLastUpdated - sProcessStart;
-                pw.format("  config runtime=%d proc=%s\n", runtime, Process.myProcessName());
-                for (int i = 0; i < sTimers.length; i++) {
-                    Timer t = sTimers[i];
-                    if (t.count != 0) {
-                        String name = sConfig.timers[i];
-                        pw.format("  stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s "
-                                        + "largest=%s\n",
-                                name, t.count, t.total / t.count, t.mintime, t.maxtime,
-                                packedString(t.percentile),
-                                packedString(t.largest));
-                    }
+        synchronized (sLock) {
+            update(refresh);
+            long runtime = sLastUpdated - sProcessStart;
+            pw.format("  config runtime=%d proc=%s\n", runtime, Process.myProcessName());
+            for (int i = 0; i < sTimers.length; i++) {
+                Timer t = sTimers[i];
+                if (t.count != 0) {
+                    String name = sConfig.timers[i];
+                    pw.format("  stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s "
+                        + "largest=%s\n",
+                        name, t.count, t.total / t.count, t.mintime, t.maxtime,
+                        packedString(t.percentile),
+                        packedString(t.largest));
                 }
             }
         }
+        pw.flush();
     }
 
     // Enable (or disabled) the runtime timers.  Note that timers are disabled by default.  This
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index cb1e016..290f526 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -232,6 +232,26 @@
     public @interface KeyGestureType {
     }
 
+    /**
+     * Returns whether the key gesture type passed as argument is allowed for visible background
+     * users.
+     *
+     * @hide
+     */
+    public static boolean isVisibleBackgrounduserAllowedGesture(int keyGestureType) {
+        switch (keyGestureType) {
+            case KEY_GESTURE_TYPE_SLEEP:
+            case KEY_GESTURE_TYPE_WAKEUP:
+            case KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
+            case KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
+            case KEY_GESTURE_TYPE_VOLUME_MUTE:
+            case KEY_GESTURE_TYPE_RECENT_APPS:
+            case KEY_GESTURE_TYPE_APP_SWITCH:
+                return false;
+        }
+        return true;
+    }
+
     public KeyGestureEvent(@NonNull AidlKeyGestureEvent keyGestureEvent) {
         this.mKeyGestureEvent = keyGestureEvent;
     }
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 0879118..4aa7462 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -593,11 +593,11 @@
      */
     public final void recycle() {
         if (mRecycled) {
-            Log.wtf(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: "
+            String error = "Recycle called on unowned Parcel. (recycle twice?) Here: "
                     + Log.getStackTraceString(new Throwable())
-                    + " Original recycle call (if DEBUG_RECYCLE): ", mStack);
-
-            return;
+                    + " Original recycle call (if DEBUG_RECYCLE): ";
+            Log.wtf(TAG, error, mStack);
+            throw new IllegalStateException(error, mStack);
         }
         mRecycled = true;
 
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 9085fe0..a58fea8 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -278,7 +278,7 @@
                 return service;
             } else {
                 return Binder.allowBlocking(
-                        getIServiceManager().checkService(name).getServiceWithMetadata().service);
+                        getIServiceManager().checkService2(name).getServiceWithMetadata().service);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "error in checkService", e);
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index 7ea521e..a5aa1b3 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -62,16 +62,23 @@
     @UnsupportedAppUsage
     public IBinder getService(String name) throws RemoteException {
         // Same as checkService (old versions of servicemanager had both methods).
-        return checkService(name).getServiceWithMetadata().service;
+        return checkService2(name).getServiceWithMetadata().service;
     }
 
     public Service getService2(String name) throws RemoteException {
         // Same as checkService (old versions of servicemanager had both methods).
-        return checkService(name);
+        return checkService2(name);
     }
 
-    public Service checkService(String name) throws RemoteException {
-        return mServiceManager.checkService(name);
+    // TODO(b/355394904): This function has been deprecated, please use checkService2 instead.
+    @UnsupportedAppUsage
+    public IBinder checkService(String name) throws RemoteException {
+        // Same as checkService (old versions of servicemanager had both methods).
+        return checkService2(name).getServiceWithMetadata().service;
+    }
+
+    public Service checkService2(String name) throws RemoteException {
+        return mServiceManager.checkService2(name);
     }
 
     public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index ebb6fb4..4a9e945 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -42,6 +42,16 @@
 }
 
 flag {
+    name: "secure_array_zeroization"
+    namespace: "platform_security"
+    description: "Enable secure array zeroization"
+    bug: "320392352"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "deprecate_fsv_sig"
     namespace: "hardware_backed_security"
     description: "Feature flag for deprecating .fsv_sig"
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index b21e85a..da3a817f 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -514,10 +514,14 @@
             final TypedArray a = resources.obtainAttributes(
                     parser, com.android.internal.R.styleable.PointerIcon);
             bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
-            hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0)
-                    * pointerScale;
-            hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0)
-                    * pointerScale;
+            // Cast the hotspot dimensions to int before scaling to match the scaling logic of
+            // the bitmap, whose intrinsic size is also an int before it is scaled.
+            final int unscaledHotSpotX =
+                    (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
+            final int unscaledHotSpotY =
+                    (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
+            hotSpotX = unscaledHotSpotX * pointerScale;
+            hotSpotY = unscaledHotSpotY * pointerScale;
             a.recycle();
         } catch (Exception ex) {
             throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 71a832d..99fe0cb 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -18,7 +18,6 @@
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.graphics.Paint.NEW_FONT_VARIATION_MANAGEMENT;
 import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT;
 import static android.view.ContentInfo.SOURCE_AUTOFILL;
 import static android.view.ContentInfo.SOURCE_CLIPBOARD;
@@ -5544,13 +5543,32 @@
             return true;
         }
 
-        final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
-                && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
         boolean effective;
-        if (useFontVariationStore) {
+        if (Flags.typefaceRedesignReadonly()) {
             if (mFontWeightAdjustment != 0
                     && mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
-                mTextPaint.setFontVariationSettings(fontVariationSettings, mFontWeightAdjustment);
+                List<FontVariationAxis> axes = FontVariationAxis.fromFontVariationSettingsForList(
+                        fontVariationSettings);
+                if (axes == null) {
+                    return false;  // invalid format of the font variation settings.
+                }
+                boolean wghtAdjusted = false;
+                for (int i = 0; i < axes.size(); ++i) {
+                    FontVariationAxis axis = axes.get(i);
+                    if (axis.getOpenTypeTagValue() == 0x77676874 /* wght */) {
+                        axes.set(i, new FontVariationAxis("wght",
+                                Math.clamp(axis.getStyleValue() + mFontWeightAdjustment,
+                                        FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
+                        wghtAdjusted = true;
+                    }
+                }
+                if (!wghtAdjusted) {
+                    axes.add(new FontVariationAxis("wght",
+                            Math.clamp(400 + mFontWeightAdjustment,
+                                    FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
+                }
+                mTextPaint.setFontVariationSettings(
+                        FontVariationAxis.toFontVariationSettings(axes));
             } else {
                 mTextPaint.setFontVariationSettings(fontVariationSettings);
             }
diff --git a/core/java/android/window/DisplayAreaOrganizer.java b/core/java/android/window/DisplayAreaOrganizer.java
index 84ce247..bd711fc 100644
--- a/core/java/android/window/DisplayAreaOrganizer.java
+++ b/core/java/android/window/DisplayAreaOrganizer.java
@@ -121,6 +121,14 @@
     public static final int FEATURE_WINDOWING_LAYER = FEATURE_SYSTEM_FIRST + 9;
 
     /**
+     * Display area for rendering app zoom out. When there are multiple layers on the screen,
+     * we want to render these layers based on a depth model. Here we zoom out the layer behind,
+     * whether it's an app or the homescreen.
+     * @hide
+     */
+    public static final int FEATURE_APP_ZOOM_OUT = FEATURE_SYSTEM_FIRST + 10;
+
+    /**
      * The last boundary of display area for system features
      */
     public static final int FEATURE_SYSTEM_LAST = 10_000;
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index de3e0d3..7a1078f 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -415,6 +415,17 @@
 }
 
 flag {
+    name: "keep_app_window_hide_while_locked"
+    namespace: "windowing_frontend"
+    description: "Do not let app window visible while device is locked"
+    is_fixed_read_only: true
+    bug: "378088391"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "port_window_size_animation"
     namespace: "windowing_frontend"
     description: "Port window-resize animation from legacy to shell"
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index c9c4be1..dc440e3 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -19,6 +19,7 @@
 import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_FINISH;
 import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_START;
 import static android.os.BatteryStats.HistoryItem.EVENT_STATE_CHANGE;
+import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -215,6 +216,7 @@
     private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>();
     private byte mLastHistoryStepLevel = 0;
     private boolean mMutable = true;
+    private int mIteratorCookie;
     private final BatteryStatsHistory mWritableHistory;
 
     private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> {
@@ -289,6 +291,7 @@
         }
 
         void load() {
+            Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
             mDirectory.mkdirs();
             if (!mDirectory.exists()) {
                 Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath());
@@ -325,8 +328,11 @@
                         }
                     } finally {
                         unlock();
+                        Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
                     }
                 });
+            } else {
+                Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
             }
         }
 
@@ -418,6 +424,7 @@
         }
 
         void writeToParcel(Parcel out, boolean useBlobs) {
+            Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel");
             lock();
             try {
                 final long start = SystemClock.uptimeMillis();
@@ -443,6 +450,7 @@
                 }
             } finally {
                 unlock();
+                Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
             }
         }
 
@@ -482,34 +490,39 @@
         }
 
         private void cleanup() {
-            if (mDirectory == null) {
-                return;
-            }
-
-            if (!tryLock()) {
-                mCleanupNeeded = true;
-                return;
-            }
-
-            mCleanupNeeded = false;
+            Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup");
             try {
-                // if free disk space is less than 100MB, delete oldest history file.
-                if (!hasFreeDiskSpace(mDirectory)) {
-                    BatteryHistoryFile oldest = mHistoryFiles.remove(0);
-                    oldest.atomicFile.delete();
+                if (mDirectory == null) {
+                    return;
                 }
 
-                // if there is more history stored than allowed, delete oldest history files.
-                int size = getSize();
-                while (size > mMaxHistorySize) {
-                    BatteryHistoryFile oldest = mHistoryFiles.get(0);
-                    int length = (int) oldest.atomicFile.getBaseFile().length();
-                    oldest.atomicFile.delete();
-                    mHistoryFiles.remove(0);
-                    size -= length;
+                if (!tryLock()) {
+                    mCleanupNeeded = true;
+                    return;
+                }
+
+                mCleanupNeeded = false;
+                try {
+                    // if free disk space is less than 100MB, delete oldest history file.
+                    if (!hasFreeDiskSpace(mDirectory)) {
+                        BatteryHistoryFile oldest = mHistoryFiles.remove(0);
+                        oldest.atomicFile.delete();
+                    }
+
+                    // if there is more history stored than allowed, delete oldest history files.
+                    int size = getSize();
+                    while (size > mMaxHistorySize) {
+                        BatteryHistoryFile oldest = mHistoryFiles.get(0);
+                        int length = (int) oldest.atomicFile.getBaseFile().length();
+                        oldest.atomicFile.delete();
+                        mHistoryFiles.remove(0);
+                        size -= length;
+                    }
+                } finally {
+                    unlock();
                 }
             } finally {
-                unlock();
+                Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
             }
         }
     }
@@ -710,13 +723,18 @@
      * in the system directory, so it is not safe while actively writing history.
      */
     public BatteryStatsHistory copy() {
-        synchronized (this) {
-            // Make a copy of battery history to avoid concurrent modification.
-            Parcel historyBufferCopy = Parcel.obtain();
-            historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
+        Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.copy");
+        try {
+            synchronized (this) {
+                // Make a copy of battery history to avoid concurrent modification.
+                Parcel historyBufferCopy = Parcel.obtain();
+                historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
 
-            return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null,
-                    null, mEventLogger, this);
+                return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null,
+                        null, null, mEventLogger, this);
+            }
+        } finally {
+            Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
         }
     }
 
@@ -826,7 +844,7 @@
      */
     @NonNull
     public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) {
-        if (mMutable) {
+        if (mMutable || mIteratorCookie != 0) {
             return copy().iterate(startTimeMs, endTimeMs);
         }
 
@@ -837,7 +855,12 @@
         mCurrentParcel = null;
         mCurrentParcelEnd = 0;
         mParcelIndex = 0;
-        return new BatteryStatsHistoryIterator(this, startTimeMs, endTimeMs);
+        BatteryStatsHistoryIterator iterator = new BatteryStatsHistoryIterator(
+                this, startTimeMs, endTimeMs);
+        mIteratorCookie = System.identityHashCode(iterator);
+        Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate",
+                mIteratorCookie);
+        return iterator;
     }
 
     /**
@@ -848,6 +871,9 @@
         if (mHistoryDir != null) {
             mHistoryDir.unlock();
         }
+        Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate",
+                mIteratorCookie);
+        mIteratorCookie = 0;
     }
 
     /**
@@ -949,28 +975,33 @@
      * @return true if success, false otherwise.
      */
     public boolean readFileToParcel(Parcel out, AtomicFile file) {
-        byte[] raw = null;
+        Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read");
         try {
-            final long start = SystemClock.uptimeMillis();
-            raw = file.readFully();
-            if (DEBUG) {
-                Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
-                        + " duration ms:" + (SystemClock.uptimeMillis() - start));
+            byte[] raw = null;
+            try {
+                final long start = SystemClock.uptimeMillis();
+                raw = file.readFully();
+                if (DEBUG) {
+                    Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
+                            + " duration ms:" + (SystemClock.uptimeMillis() - start));
+                }
+            } catch (Exception e) {
+                Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
+                return false;
             }
-        } catch (Exception e) {
-            Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
-            return false;
+            out.unmarshall(raw, 0, raw.length);
+            out.setDataPosition(0);
+            if (!verifyVersion(out)) {
+                return false;
+            }
+            // skip monotonic time field.
+            out.readLong();
+            // skip monotonic size field
+            out.readLong();
+            return true;
+        } finally {
+            Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
         }
-        out.unmarshall(raw, 0, raw.length);
-        out.setDataPosition(0);
-        if (!verifyVersion(out)) {
-            return false;
-        }
-        // skip monotonic time field.
-        out.readLong();
-        // skip monotonic size field
-        out.readLong();
-        return true;
     }
 
     /**
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index f14e1f6..ec0954d 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -239,4 +239,7 @@
 
     /** Unbundle a categorized notification */
     void unbundleNotification(String key);
+
+    /** Rebundle an (un)categorized notification */
+    void rebundleNotification(String key);
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 39ddea6..7470770 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -65,6 +65,7 @@
 import android.view.InputDevice;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 import com.android.server.LocalServices;
 
 import com.google.android.collect.Lists;
@@ -75,6 +76,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -292,6 +294,56 @@
 
     }
 
+    /**
+     * This exists temporarily due to trunk-stable policies.
+     * Please use ArrayUtils directly if you can.
+     */
+    public static byte[] newNonMovableByteArray(int length) {
+        if (!android.security.Flags.secureArrayZeroization()) {
+            return new byte[length];
+        }
+        return ArrayUtils.newNonMovableByteArray(length);
+    }
+
+    /**
+     * This exists temporarily due to trunk-stable policies.
+     * Please use ArrayUtils directly if you can.
+     */
+    public static char[] newNonMovableCharArray(int length) {
+        if (!android.security.Flags.secureArrayZeroization()) {
+            return new char[length];
+        }
+        return ArrayUtils.newNonMovableCharArray(length);
+    }
+
+    /**
+     * This exists temporarily due to trunk-stable policies.
+     * Please use ArrayUtils directly if you can.
+     */
+    public static void zeroize(byte[] array) {
+        if (!android.security.Flags.secureArrayZeroization()) {
+            if (array != null) {
+                Arrays.fill(array, (byte) 0);
+            }
+            return;
+        }
+        ArrayUtils.zeroize(array);
+    }
+
+    /**
+     * This exists temporarily due to trunk-stable policies.
+     * Please use ArrayUtils directly if you can.
+     */
+    public static void zeroize(char[] array) {
+        if (!android.security.Flags.secureArrayZeroization()) {
+            if (array != null) {
+                Arrays.fill(array, (char) 0);
+            }
+            return;
+        }
+        ArrayUtils.zeroize(array);
+    }
+
     @UnsupportedAppUsage
     public DevicePolicyManager getDevicePolicyManager() {
         if (mDevicePolicyManager == null) {
diff --git a/core/java/com/android/internal/widget/LockscreenCredential.java b/core/java/com/android/internal/widget/LockscreenCredential.java
index 54b9a22..92ce990 100644
--- a/core/java/com/android/internal/widget/LockscreenCredential.java
+++ b/core/java/com/android/internal/widget/LockscreenCredential.java
@@ -246,7 +246,7 @@
      */
     public void zeroize() {
         if (mCredential != null) {
-            Arrays.fill(mCredential, (byte) 0);
+            LockPatternUtils.zeroize(mCredential);
             mCredential = null;
         }
     }
@@ -346,7 +346,7 @@
             byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword);
             byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword);
 
-            Arrays.fill(saltedPassword, (byte) 0);
+            LockPatternUtils.zeroize(saltedPassword);
             return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5));
         } catch (NoSuchAlgorithmException e) {
             throw new AssertionError("Missing digest algorithm: ", e);
diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java
index 80bc4fd..dd12f69 100644
--- a/core/java/com/android/internal/widget/NotificationExpandButton.java
+++ b/core/java/com/android/internal/widget/NotificationExpandButton.java
@@ -56,8 +56,6 @@
     private int mDefaultTextColor;
     private int mHighlightPillColor;
     private int mHighlightTextColor;
-    // Track whether this ever had mExpanded = true, so that we don't highlight it anymore.
-    private boolean mWasExpanded = false;
 
     public NotificationExpandButton(Context context) {
         this(context, null, 0, 0);
@@ -136,7 +134,6 @@
         int contentDescriptionId;
         if (mExpanded) {
             if (notificationsRedesignTemplates()) {
-                mWasExpanded = true;
                 drawableId = R.drawable.ic_notification_2025_collapse;
             } else {
                 drawableId = R.drawable.ic_collapse_notification;
@@ -156,8 +153,6 @@
         if (!notificationsRedesignTemplates()) {
             // changing the expanded state can affect the number display
             updateNumber();
-        } else {
-            updateColors();
         }
     }
 
@@ -197,43 +192,22 @@
         );
     }
 
-    /**
-     * Use highlight colors for the expander for groups (when the number is showing) that haven't
-     * been opened before, as long as the colors are available.
-     */
-    private boolean shouldBeHighlighted() {
-        return !mWasExpanded && shouldShowNumber()
-                && mHighlightPillColor != 0 && mHighlightTextColor != 0;
-    }
-
     private void updateColors() {
-        if (notificationsRedesignTemplates()) {
-            if (shouldBeHighlighted()) {
+        if (shouldShowNumber()) {
+            if (mHighlightPillColor != 0) {
                 mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor));
-                mIconView.setColorFilter(mHighlightTextColor);
+            }
+            mIconView.setColorFilter(mHighlightTextColor);
+            if (mHighlightTextColor != 0) {
                 mNumberView.setTextColor(mHighlightTextColor);
-            } else {
-                mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
-                mIconView.setColorFilter(mDefaultTextColor);
-                mNumberView.setTextColor(mDefaultTextColor);
             }
         } else {
-            if (shouldShowNumber()) {
-                if (mHighlightPillColor != 0) {
-                    mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor));
-                }
-                mIconView.setColorFilter(mHighlightTextColor);
-                if (mHighlightTextColor != 0) {
-                    mNumberView.setTextColor(mHighlightTextColor);
-                }
-            } else {
-                if (mDefaultPillColor != 0) {
-                    mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
-                }
-                mIconView.setColorFilter(mDefaultTextColor);
-                if (mDefaultTextColor != 0) {
-                    mNumberView.setTextColor(mDefaultTextColor);
-                }
+            if (mDefaultPillColor != 0) {
+                mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
+            }
+            mIconView.setColorFilter(mDefaultTextColor);
+            if (mDefaultTextColor != 0) {
+                mNumberView.setTextColor(mDefaultTextColor);
             }
         }
     }
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
index 8cd7843..f0b5493 100644
--- a/core/java/com/android/internal/widget/NotificationProgressBar.java
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -23,6 +23,7 @@
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -31,6 +32,7 @@
 import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.Pair;
 import android.view.RemotableViewMethod;
 import android.widget.ProgressBar;
 import android.widget.RemoteViews;
@@ -40,14 +42,12 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
-import com.android.internal.widget.NotificationProgressDrawable.Part;
-import com.android.internal.widget.NotificationProgressDrawable.Point;
-import com.android.internal.widget.NotificationProgressDrawable.Segment;
 
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
@@ -56,18 +56,25 @@
  * represent Notification ProgressStyle progress, such as for ridesharing and navigation.
  */
 @RemoteViews.RemoteView
-public final class NotificationProgressBar extends ProgressBar {
+public final class NotificationProgressBar extends ProgressBar implements
+        NotificationProgressDrawable.BoundsChangeListener {
     private static final String TAG = "NotificationProgressBar";
 
     private NotificationProgressDrawable mNotificationProgressDrawable;
+    private final Rect mProgressDrawableBounds = new Rect();
 
     private NotificationProgressModel mProgressModel;
 
     @Nullable
-    private List<Part> mProgressDrawableParts = null;
+    private List<Part> mParts = null;
+
+    // List of drawable parts before segment splitting by process.
+    @Nullable
+    private List<NotificationProgressDrawable.Part> mProgressDrawableParts = null;
 
     @Nullable
     private Drawable mTracker = null;
+    private boolean mHasTrackerIcon = false;
 
     /** @see R.styleable#NotificationProgressBar_trackerHeight */
     private final int mTrackerHeight;
@@ -76,7 +83,13 @@
     private final Matrix mMatrix = new Matrix();
     private Matrix mTrackerDrawMatrix = null;
 
-    private float mScale = 0;
+    private float mProgressFraction = 0;
+    /**
+     * The location of progress on the stretched and rescaled progress bar, in fraction. Used for
+     * calculating the tracker position. If stretching and rescaling is not needed, ==
+     * mProgressFraction.
+     */
+    private float mAdjustedProgressFraction = 0;
     /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */
     private boolean mTrackerPosIsDirty = false;
 
@@ -104,12 +117,13 @@
 
         try {
             mNotificationProgressDrawable = getNotificationProgressDrawable();
+            mNotificationProgressDrawable.setBoundsChangeListener(this);
         } catch (IllegalStateException ex) {
             Log.e(TAG, "Can't get NotificationProgressDrawable", ex);
         }
 
         // Supports setting the tracker in xml, but ProgressStyle notifications set/override it
-        // via {@code setProgressTrackerIcon}.
+        // via {@code #setProgressTrackerIcon}.
         final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker);
         setTracker(tracker);
 
@@ -137,20 +151,25 @@
             final int indeterminateColor = mProgressModel.getIndeterminateColor();
             setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor));
         } else {
+            // TODO: b/372908709 - maybe don't rerun the entire calculation every time the
+            //  progress model is updated? For example, if the segments and parts aren't changed,
+            //  there is no need to call `processAndConvertToViewParts` again.
+
             final int progress = mProgressModel.getProgress();
             final int progressMax = mProgressModel.getProgressMax();
-            mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(),
+
+            mParts = processAndConvertToViewParts(mProgressModel.getSegments(),
                     mProgressModel.getPoints(),
                     progress,
-                    progressMax,
-                    mProgressModel.isStyledByProgress());
-
-            if (mNotificationProgressDrawable != null) {
-                mNotificationProgressDrawable.setParts(mProgressDrawableParts);
-            }
+                    progressMax);
 
             setMax(progressMax);
             setProgress(progress);
+
+            if (mNotificationProgressDrawable != null
+                    && mNotificationProgressDrawable.getBounds().width() != 0) {
+                updateDrawableParts();
+            }
         }
     }
 
@@ -200,9 +219,7 @@
         } else {
             progressTrackerDrawable = null;
         }
-        return () -> {
-            setTracker(progressTrackerDrawable);
-        };
+        return () -> setTracker(progressTrackerDrawable);
     }
 
     private void setTracker(@Nullable Drawable tracker) {
@@ -226,8 +243,14 @@
         final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker);
 
         mTracker = tracker;
-        if (mNotificationProgressDrawable != null) {
-            mNotificationProgressDrawable.setHasTrackerIcon(mTracker != null);
+        final boolean hasTrackerIcon = (mTracker != null);
+        if (mHasTrackerIcon != hasTrackerIcon) {
+            mHasTrackerIcon = hasTrackerIcon;
+            if (mNotificationProgressDrawable != null
+                    && mNotificationProgressDrawable.getBounds().width() != 0
+                    && mProgressModel.isStyledByProgress()) {
+                updateDrawableParts();
+            }
         }
 
         configureTrackerBounds();
@@ -293,6 +316,8 @@
         mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
     }
 
+    // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+    // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
     @Override
     public synchronized void setProgress(int progress) {
         super.setProgress(progress);
@@ -300,6 +325,8 @@
         onMaybeVisualProgressChanged();
     }
 
+    // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+    // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
     @Override
     public void setProgress(int progress, boolean animate) {
         // Animation isn't supported by NotificationProgressBar.
@@ -308,6 +335,8 @@
         onMaybeVisualProgressChanged();
     }
 
+    // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+    // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
     @Override
     public synchronized void setMin(int min) {
         super.setMin(min);
@@ -315,6 +344,8 @@
         onMaybeVisualProgressChanged();
     }
 
+    // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+    // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
     @Override
     public synchronized void setMax(int max) {
         super.setMax(max);
@@ -323,10 +354,10 @@
     }
 
     private void onMaybeVisualProgressChanged() {
-        float scale = getScale();
-        if (mScale == scale) return;
+        float progressFraction = getProgressFraction();
+        if (mProgressFraction == progressFraction) return;
 
-        mScale = scale;
+        mProgressFraction = progressFraction;
         mTrackerPosIsDirty = true;
         invalidate();
     }
@@ -372,6 +403,59 @@
         updateTrackerAndBarPos(w, h);
     }
 
+    @Override
+    public void onDrawableBoundsChanged() {
+        final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds();
+
+        if (mProgressDrawableBounds.equals(progressDrawableBounds)) return;
+
+        if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) {
+            updateDrawableParts();
+        }
+
+        mProgressDrawableBounds.set(progressDrawableBounds);
+    }
+
+    private void updateDrawableParts() {
+        Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = "
+                + mNotificationProgressDrawable + ", mParts = " + mParts);
+
+        if (mNotificationProgressDrawable == null) return;
+        if (mParts == null) return;
+
+        final float width = mNotificationProgressDrawable.getBounds().width();
+        if (width == 0) {
+            if (mProgressDrawableParts != null) {
+                Log.d(TAG, "Clearing mProgressDrawableParts");
+                mProgressDrawableParts.clear();
+                mNotificationProgressDrawable.setParts(mProgressDrawableParts);
+            }
+            return;
+        }
+
+        mProgressDrawableParts = processAndConvertToDrawableParts(
+                mParts,
+                width,
+                mNotificationProgressDrawable.getSegSegGap(),
+                mNotificationProgressDrawable.getSegPointGap(),
+                mNotificationProgressDrawable.getPointRadius(),
+                mHasTrackerIcon
+        );
+        Pair<List<NotificationProgressDrawable.Part>, Float> p = maybeStretchAndRescaleSegments(
+                mParts,
+                mProgressDrawableParts,
+                mNotificationProgressDrawable.getSegmentMinWidth(),
+                mNotificationProgressDrawable.getPointRadius(),
+                getProgressFraction(),
+                width,
+                mProgressModel.isStyledByProgress(),
+                mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap());
+
+        Log.d(TAG, "Updating NotificationProgressDrawable parts");
+        mNotificationProgressDrawable.setParts(p.first);
+        mAdjustedProgressFraction = p.second / width;
+    }
+
     private void updateTrackerAndBarPos(int w, int h) {
         final int paddedHeight = h - mPaddingTop - mPaddingBottom;
         final Drawable bar = getCurrentDrawable();
@@ -402,11 +486,11 @@
         }
 
         if (tracker != null) {
-            setTrackerPos(w, tracker, mScale, trackerOffsetY);
+            setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY);
         }
     }
 
-    private float getScale() {
+    private float getProgressFraction() {
         int min = getMin();
         int max = getMax();
         int range = max - min;
@@ -418,17 +502,17 @@
      *
      * @param w Width of the view, including padding
      * @param tracker Drawable used for the tracker
-     * @param scale Current progress between 0 and 1
+     * @param progressFraction Current progress between 0 and 1
      * @param offsetY Vertical offset for centering. If set to
      *            {@link Integer#MIN_VALUE}, the current offset will be used.
      */
-    private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) {
+    private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) {
         int available = w - mPaddingLeft - mPaddingRight;
         final int trackerWidth = tracker.getIntrinsicWidth();
         final int trackerHeight = tracker.getIntrinsicHeight();
         available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth);
 
-        final int trackerPos = (int) (scale * available + 0.5f);
+        final int trackerPos = (int) (progressFraction * available + 0.5f);
 
         final int top, bottom;
         if (offsetY == Integer.MIN_VALUE) {
@@ -482,7 +566,7 @@
         if (mTracker == null) return;
 
         if (mTrackerPosIsDirty) {
-            setTrackerPos(getWidth(), mTracker, mScale, Integer.MIN_VALUE);
+            setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE);
         }
 
         final int saveCount = canvas.save();
@@ -531,7 +615,7 @@
 
         final Drawable tracker = mTracker;
         if (tracker != null) {
-            setTrackerPos(getWidth(), tracker, mScale, Integer.MIN_VALUE);
+            setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE);
 
             // Since we draw translated, the drawable's bounds that it signals
             // for invalidation won't be the actual bounds we want invalidated,
@@ -541,16 +625,14 @@
     }
 
     /**
-     * Processes the ProgressStyle data and convert to list of {@code
-     * NotificationProgressDrawable.Part}.
+     * Processes the ProgressStyle data and convert to a list of {@code Part}.
      */
     @VisibleForTesting
-    public static List<Part> processAndConvertToDrawableParts(
+    public static List<Part> processAndConvertToViewParts(
             List<ProgressStyle.Segment> segments,
             List<ProgressStyle.Point> points,
             int progress,
-            int progressMax,
-            boolean isStyledByProgress
+            int progressMax
     ) {
         if (segments.isEmpty()) {
             throw new IllegalArgumentException("List of segments shouldn't be empty");
@@ -571,6 +653,7 @@
         if (progress < 0 || progress > progressMax) {
             throw new IllegalArgumentException("Invalid progress : " + progress);
         }
+
         for (ProgressStyle.Point point : points) {
             final int pos = point.getPosition();
             if (pos < 0 || pos > progressMax) {
@@ -583,23 +666,21 @@
         final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap(
                 points);
         final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap,
-                positionToPointMap, progress, isStyledByProgress);
+                positionToPointMap);
 
         final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap =
-                splitSegmentsByPointsAndProgress(
-                        startToSegmentMap, sortedPos, progressMax);
+                splitSegmentsByPoints(startToSegmentMap, sortedPos, progressMax);
 
-        return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
-                progress, progressMax,
-                isStyledByProgress);
+        return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
+                progressMax);
     }
 
     // Any segment with a point on it gets split by the point.
-    // If isStyledByProgress is true, also split the segment with the progress value in its range.
-    private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress(
+    private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints(
             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
             SortedSet<Integer> sortedPos,
-            int progressMax) {
+            int progressMax
+    ) {
         int prevSegStart = 0;
         for (Integer pos : sortedPos) {
             if (pos == 0 || pos == progressMax) continue;
@@ -624,32 +705,22 @@
         return startToSegmentMap;
     }
 
-    private static List<Part> convertToDrawableParts(
+    private static List<Part> convertToViewParts(
             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
             Map<Integer, ProgressStyle.Point> positionToPointMap,
             SortedSet<Integer> sortedPos,
-            int progress,
-            int progressMax,
-            boolean isStyledByProgress
+            int progressMax
     ) {
         List<Part> parts = new ArrayList<>();
-        boolean styleRemainingParts = false;
         for (Integer pos : sortedPos) {
             if (positionToPointMap.containsKey(pos)) {
                 final ProgressStyle.Point point = positionToPointMap.get(pos);
-                final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts);
-                parts.add(new Point(null, color, styleRemainingParts));
-            }
-            // We want the Point at the current progress to be filled (not faded), but a Segment
-            // starting at this progress to be faded.
-            if (isStyledByProgress && !styleRemainingParts && pos == progress) {
-                styleRemainingParts = true;
+                parts.add(new Point(point.getColor()));
             }
             if (startToSegmentMap.containsKey(pos)) {
                 final ProgressStyle.Segment seg = startToSegmentMap.get(pos);
-                final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts);
                 parts.add(new Segment(
-                        (float) seg.getLength() / progressMax, color, styleRemainingParts));
+                        (float) seg.getLength() / progressMax, seg.getColor()));
             }
         }
 
@@ -660,11 +731,24 @@
     private static int maybeGetFadedColor(@ColorInt int color, boolean fade) {
         if (!fade) return color;
 
-        return NotificationProgressDrawable.getFadedColor(color);
+        return getFadedColor(color);
+    }
+
+    /**
+     * Get a color with an opacity that's 40% of the input color.
+     */
+    @ColorInt
+    static int getFadedColor(@ColorInt int color) {
+        return Color.argb(
+                (int) (Color.alpha(color) * 0.4f + 0.5f),
+                Color.red(color),
+                Color.green(color),
+                Color.blue(color));
     }
 
     private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap(
-            List<ProgressStyle.Segment> segments) {
+            List<ProgressStyle.Segment> segments
+    ) {
         final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>();
 
         int currentStart = 0;  // Initial start position is 0
@@ -681,7 +765,8 @@
     }
 
     private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap(
-            List<ProgressStyle.Point> points) {
+            List<ProgressStyle.Point> points
+    ) {
         final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>();
 
         for (ProgressStyle.Point point : points) {
@@ -693,14 +778,404 @@
 
     private static SortedSet<Integer> generateSortedPositionSet(
             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
-            Map<Integer, ProgressStyle.Point> positionToPointMap, int progress,
-            boolean isStyledByProgress) {
+            Map<Integer, ProgressStyle.Point> positionToPointMap
+    ) {
         final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet());
         sortedPos.addAll(positionToPointMap.keySet());
-        if (isStyledByProgress) {
-            sortedPos.add(progress);
-        }
 
         return sortedPos;
     }
+
+    /**
+     * Processes the list of {@code Part} and convert to a list of
+     * {@code NotificationProgressDrawable.Part}.
+     */
+    @VisibleForTesting
+    public static List<NotificationProgressDrawable.Part> processAndConvertToDrawableParts(
+            List<Part> parts,
+            float totalWidth,
+            float segSegGap,
+            float segPointGap,
+            float pointRadius,
+            boolean hasTrackerIcon
+    ) {
+        List<NotificationProgressDrawable.Part> drawableParts = new ArrayList<>();
+
+        // generally, we will start drawing at (x, y) and end at (x+w, y)
+        float x = (float) 0;
+
+        final int nParts = parts.size();
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final Part part = parts.get(iPart);
+            final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1);
+            final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1);
+            if (part instanceof Segment segment) {
+                final float segWidth = segment.mFraction * totalWidth;
+                // Advance the start position to account for a point immediately prior.
+                final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x);
+                final float start = x + startOffset;
+                // Retract the end position to account for the padding and a point immediately
+                // after.
+                final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
+                        segSegGap, x + segWidth, totalWidth, hasTrackerIcon);
+                final float end = x + segWidth - endOffset;
+
+                drawableParts.add(
+                        new NotificationProgressDrawable.Segment(start, end, segment.mColor,
+                                segment.mFaded));
+
+                segment.mStart = x;
+                segment.mEnd = x + segWidth;
+
+                // Advance the current position to account for the segment's fraction of the total
+                // width (ignoring offset and padding)
+                x += segWidth;
+            } else if (part instanceof Point point) {
+                final float pointWidth = 2 * pointRadius;
+                float start = x - pointRadius;
+                if (start < 0) start = 0;
+                float end = start + pointWidth;
+                if (end > totalWidth) {
+                    end = totalWidth;
+                    if (totalWidth > pointWidth) start = totalWidth - pointWidth;
+                }
+
+                drawableParts.add(
+                        new NotificationProgressDrawable.Point(start, end, point.mColor));
+            }
+        }
+
+        return drawableParts;
+    }
+
+    private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap,
+            float startX) {
+        if (!(prevPart instanceof Point)) return 0F;
+        final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0;
+        return pointOffset + pointRadius + segPointGap;
+    }
+
+    private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
+            float segPointGap,
+            float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) {
+        if (nextPart == null) return 0F;
+        if (nextPart instanceof Segment nextSeg) {
+            if (!seg.mFaded && nextSeg.mFaded) {
+                // @see Segment#mFaded
+                return hasTrackerIcon ? 0F : segSegGap;
+            }
+            return segSegGap;
+        }
+
+        final float pointWidth = 2 * pointRadius;
+        final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth)
+                ? (endX + pointRadius - totalWidth) : 0;
+        return segPointGap + pointRadius + pointOffset;
+    }
+
+    /**
+     * Processes the list of {@code NotificationProgressBar.Part} data and convert to a pair of:
+     *   - list of {@code NotificationProgressDrawable.Part}.
+     *   - location of progress on the stretched and rescaled progress bar.
+     */
+    @VisibleForTesting
+    public static Pair<List<NotificationProgressDrawable.Part>, Float>
+                maybeStretchAndRescaleSegments(
+            List<Part> parts,
+            List<NotificationProgressDrawable.Part> drawableParts,
+            float segmentMinWidth,
+            float pointRadius,
+            float progressFraction,
+            float totalWidth,
+            boolean isStyledByProgress,
+            float progressGap
+    ) {
+        final List<NotificationProgressDrawable.Segment> drawableSegments = drawableParts
+                .stream()
+                .filter(NotificationProgressDrawable.Segment.class::isInstance)
+                .map(NotificationProgressDrawable.Segment.class::cast)
+                .toList();
+        float totalExcessWidth = 0;
+        float totalPositiveExcessWidth = 0;
+        for (NotificationProgressDrawable.Segment drawableSegment : drawableSegments) {
+            final float excessWidth = drawableSegment.getWidth() - segmentMinWidth;
+            totalExcessWidth += excessWidth;
+            if (excessWidth > 0) totalPositiveExcessWidth += excessWidth;
+        }
+
+        // All drawable segments are above minimum width. No need to stretch and rescale.
+        if (totalExcessWidth == totalPositiveExcessWidth) {
+            return maybeSplitDrawableSegmentsByProgress(
+                    parts,
+                    drawableParts,
+                    progressFraction,
+                    totalWidth,
+                    isStyledByProgress,
+                    progressGap);
+        }
+
+        if (totalExcessWidth < 0) {
+            // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback
+            //  option. (instead of return.)
+            Log.w(TAG, "Not enough width to satisfy the minimum width for segments.");
+            return maybeSplitDrawableSegmentsByProgress(
+                    parts,
+                    drawableParts,
+                    progressFraction,
+                    totalWidth,
+                    isStyledByProgress,
+                    progressGap);
+        }
+
+        final int nParts = drawableParts.size();
+        float startOffset = 0;
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart);
+            if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) {
+                final float origDrawableSegmentWidth = drawableSegment.getWidth();
+
+                float drawableSegmentWidth = segmentMinWidth;
+                // Allocate the totalExcessWidth to the segments above minimum, proportionally to
+                // their initial excessWidth.
+                if (origDrawableSegmentWidth > segmentMinWidth) {
+                    drawableSegmentWidth +=
+                            totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth)
+                                    / totalPositiveExcessWidth;
+                }
+
+                final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth();
+
+                // Adjust drawable segments to new widths
+                drawableSegment.setStart(drawableSegment.getStart() + startOffset);
+                drawableSegment.setEnd(
+                        drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff);
+
+                // Also adjust view segments to new width. (For view segments, only start is
+                // needed?)
+                // Check that segments and drawableSegments are of the same size?
+                final Segment segment = (Segment) parts.get(iPart);
+                final float origSegmentWidth = segment.getWidth();
+                segment.mStart = segment.mStart + startOffset;
+                segment.mEnd = segment.mStart + origSegmentWidth + widthDiff;
+
+                // Increase startOffset for the subsequent segments.
+                startOffset += widthDiff;
+            } else if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) {
+                drawablePoint.setStart(drawablePoint.getStart() + startOffset);
+                drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius);
+            }
+        }
+
+        return maybeSplitDrawableSegmentsByProgress(
+                parts,
+                drawableParts,
+                progressFraction,
+                totalWidth,
+                isStyledByProgress,
+                progressGap);
+    }
+
+    // Find the location of progress on the stretched and rescaled progress bar.
+    // If isStyledByProgress is true, also split the segment with the progress value in its range.
+    private static Pair<List<NotificationProgressDrawable.Part>, Float>
+                maybeSplitDrawableSegmentsByProgress(
+            // Needed to get the original segment start and end positions in pixels.
+            List<Part> parts,
+            List<NotificationProgressDrawable.Part> drawableParts,
+            float progressFraction,
+            float totalWidth,
+            boolean isStyledByProgress,
+            float progressGap
+    ) {
+        if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth);
+
+        int iPartFirstSegmentToStyle = -1;
+        int iPartSegmentToSplit = -1;
+        float rescaledProgressX = 0;
+        float startFraction = 0;
+        final int nParts = parts.size();
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final Part part = parts.get(iPart);
+            if (!(part instanceof Segment)) continue;
+            final Segment segment = (Segment) part;
+            if (startFraction == progressFraction) {
+                iPartFirstSegmentToStyle = iPart;
+                rescaledProgressX = segment.mStart;
+                break;
+            } else if (startFraction < progressFraction
+                    && progressFraction < startFraction + segment.mFraction) {
+                iPartSegmentToSplit = iPart;
+                rescaledProgressX =
+                        segment.mStart + (progressFraction - startFraction) / segment.mFraction
+                                * segment.getWidth();
+                break;
+            }
+            startFraction += segment.mFraction;
+        }
+
+        if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX);
+
+        List<NotificationProgressDrawable.Part> splitDrawableParts = new ArrayList<>();
+        boolean styleRemainingParts = false;
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart);
+            if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) {
+                final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts);
+                splitDrawableParts.add(
+                        new NotificationProgressDrawable.Point(drawablePoint.getStart(),
+                                drawablePoint.getEnd(), color));
+            }
+            if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true;
+            if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) {
+                if (iPart == iPartSegmentToSplit) {
+                    if (rescaledProgressX <= drawableSegment.getStart()) {
+                        styleRemainingParts = true;
+                        final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
+                        splitDrawableParts.add(
+                                new NotificationProgressDrawable.Segment(drawableSegment.getStart(),
+                                        drawableSegment.getEnd(),
+                                        color, true));
+                    } else if (drawableSegment.getStart() < rescaledProgressX
+                            && rescaledProgressX < drawableSegment.getEnd()) {
+                        splitDrawableParts.add(
+                                new NotificationProgressDrawable.Segment(drawableSegment.getStart(),
+                                        rescaledProgressX - progressGap,
+                                        drawableSegment.getColor()));
+                        final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
+                        splitDrawableParts.add(
+                                new NotificationProgressDrawable.Segment(rescaledProgressX,
+                                        drawableSegment.getEnd(), color, true));
+                        styleRemainingParts = true;
+                    } else {
+                        splitDrawableParts.add(
+                                new NotificationProgressDrawable.Segment(drawableSegment.getStart(),
+                                        drawableSegment.getEnd(),
+                                        drawableSegment.getColor()));
+                        styleRemainingParts = true;
+                    }
+                } else {
+                    final int color = maybeGetFadedColor(drawableSegment.getColor(),
+                            styleRemainingParts);
+                    splitDrawableParts.add(
+                            new NotificationProgressDrawable.Segment(drawableSegment.getStart(),
+                                    drawableSegment.getEnd(),
+                                    color, styleRemainingParts));
+                }
+            }
+        }
+
+        return new Pair<>(splitDrawableParts, rescaledProgressX);
+    }
+
+    /**
+     * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a
+     * {@link Point} with zero length.
+     */
+    // TODO: b/372908709 - maybe this should be made private? Only test the final
+    //  NotificationDrawable.Parts.
+    // TODO: b/372908709 - rename to BarPart, BarSegment, BarPoint. This avoids naming ambiguity
+    //  with the types in NotificationProgressDrawable.
+    public interface Part {
+    }
+
+    /**
+     * A segment is a part of the progress bar with non-zero length. For example, it can
+     * represent a portion in a navigation journey with certain traffic condition.
+     *
+     */
+    public static final class Segment implements Part {
+        private final float mFraction;
+        @ColorInt private final int mColor;
+        /** Whether the segment is faded or not.
+         * <p>
+         *     <pre>
+         *     When mFaded is set to true, a combination of the following is done to the segment:
+         *       1. The drawing color is mColor with opacity updated to 40%.
+         *       2. The gap between faded and non-faded segments is:
+         *          - the segment-segment gap, when there is no tracker icon
+         *          - 0, when there is tracker icon
+         *     </pre>
+         * </p>
+         */
+        private final boolean mFaded;
+
+        /** Start position (in pixels) */
+        private float mStart;
+        /** End position (in pixels */
+        private float mEnd;
+
+        public Segment(float fraction, @ColorInt int color) {
+            this(fraction, color, false);
+        }
+
+        public Segment(float fraction, @ColorInt int color, boolean faded) {
+            mFraction = fraction;
+            mColor = color;
+            mFaded = faded;
+        }
+
+        /** Returns the calculated drawing width of the part */
+        public float getWidth() {
+            return mEnd - mStart;
+        }
+
+        @Override
+        public String toString() {
+            return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded="
+                    + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd;
+        }
+
+        // Needed for unit tests
+        @Override
+        public boolean equals(@androidx.annotation.Nullable Object other) {
+            if (this == other) return true;
+
+            if (other == null || getClass() != other.getClass()) return false;
+
+            Segment that = (Segment) other;
+            if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
+            if (this.mColor != that.mColor) return false;
+            return this.mFaded == that.mFaded;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mFraction, mColor, mFaded);
+        }
+    }
+
+    /**
+     * A point is a part of the progress bar with zero length. Points are designated points within a
+     * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop
+     * ride-share journey.
+     */
+    public static final class Point implements Part {
+        @ColorInt private final int mColor;
+
+        public Point(@ColorInt int color) {
+            mColor = color;
+        }
+
+        @Override
+        public String toString() {
+            return "Point(color=" + this.mColor + ")";
+        }
+
+        // Needed for unit tests.
+        @Override
+        public boolean equals(@androidx.annotation.Nullable Object other) {
+            if (this == other) return true;
+
+            if (other == null || getClass() != other.getClass()) return false;
+
+            Point that = (Point) other;
+
+            return this.mColor == that.mColor;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mColor);
+        }
+    }
 }
diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
index 8629a1c..ef0a5d5 100644
--- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java
+++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
@@ -21,7 +21,6 @@
 import android.content.res.Resources.Theme;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
@@ -49,7 +48,8 @@
 
 /**
  * This is used by NotificationProgressBar for displaying a custom background. It composes of
- * segments, which have non-zero length, and points, which have zero length.
+ * segments, which have non-zero length varying drawing width, and points, which have zero length
+ * and fixed size for drawing.
  *
  * @see Segment
  * @see Point
@@ -57,14 +57,15 @@
 public final class NotificationProgressDrawable extends Drawable {
     private static final String TAG = "NotifProgressDrawable";
 
+    @Nullable
+    private BoundsChangeListener mBoundsChangeListener = null;
+
     private State mState;
     private boolean mMutated;
 
     private final ArrayList<Part> mParts = new ArrayList<>();
-    private boolean mHasTrackerIcon;
 
     private final RectF mSegRectF = new RectF();
-    private final Rect mPointRect = new Rect();
     private final RectF mPointRectF = new RectF();
 
     private final Paint mFillPaint = new Paint();
@@ -80,27 +81,31 @@
     }
 
     /**
-     * <p>Set the segment default color for the drawable.</p>
-     * <p>Note: changing this property will affect all instances of a drawable loaded from a
-     * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p>
-     *
-     * @param color The color of the stroke
-     * @see #mutate()
+     * Returns the gap between two segments.
      */
-    public void setSegmentDefaultColor(@ColorInt int color) {
-        mState.setSegmentColor(color);
+    public float getSegSegGap() {
+        return mState.mSegSegGap;
     }
 
     /**
-     * <p>Set the point rect default color for the drawable.</p>
-     * <p>Note: changing this property will affect all instances of a drawable loaded from a
-     * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p>
-     *
-     * @param color The color of the point rect
-     * @see #mutate()
+     * Returns the gap between a segment and a point.
      */
-    public void setPointRectDefaultColor(@ColorInt int color) {
-        mState.setPointRectColor(color);
+    public float getSegPointGap() {
+        return mState.mSegPointGap;
+    }
+
+    /**
+     * Returns the gap between a segment and a point.
+     */
+    public float getSegmentMinWidth() {
+        return mState.mSegmentMinWidth;
+    }
+
+    /**
+     * Returns the radius for the points.
+     */
+    public float getPointRadius() {
+        return mState.mPointRadius;
     }
 
     /**
@@ -120,47 +125,18 @@
         setParts(Arrays.asList(parts));
     }
 
-    /**
-     * Set whether a tracker is drawn on top of this NotificationProgressDrawable.
-     */
-    public void setHasTrackerIcon(boolean hasTrackerIcon) {
-        if (mHasTrackerIcon != hasTrackerIcon) {
-            mHasTrackerIcon = hasTrackerIcon;
-            invalidateSelf();
-        }
-    }
-
     @Override
     public void draw(@NonNull Canvas canvas) {
-        final float pointRadius =
-                mState.mPointRadius; // how big the point icon will be, halved
-
-        // generally, we will start drawing at (x, y) and end at (x+w, y)
-        float x = (float) getBounds().left;
+        final float pointRadius = mState.mPointRadius;
+        final float left = (float) getBounds().left;
         final float centerY = (float) getBounds().centerY();
-        final float totalWidth = (float) getBounds().width();
-        float segPointGap = mState.mSegPointGap;
 
         final int numParts = mParts.size();
         for (int iPart = 0; iPart < numParts; iPart++) {
             final Part part = mParts.get(iPart);
-            final Part prevPart = iPart == 0 ? null : mParts.get(iPart - 1);
-            final Part nextPart = iPart + 1 == numParts ? null : mParts.get(iPart + 1);
+            final float start = left + part.mStart;
+            final float end = left + part.mEnd;
             if (part instanceof Segment segment) {
-                final float segWidth = segment.mFraction * totalWidth;
-                // Advance the start position to account for a point immediately prior.
-                final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x);
-                final float start = x + startOffset;
-                // Retract the end position to account for the padding and a point immediately
-                // after.
-                final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
-                        mState.mSegSegGap, x + segWidth, totalWidth, mHasTrackerIcon);
-                final float end = x + segWidth - endOffset;
-
-                // Advance the current position to account for the segment's fraction of the total
-                // width (ignoring offset and padding)
-                x += segWidth;
-
                 // No space left to draw the segment
                 if (start > end) continue;
 
@@ -168,69 +144,25 @@
                         : mState.mSegmentHeight / 2F;
                 final float cornerRadius = mState.mSegmentCornerRadius;
 
-                mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
-                        : (segment.mFaded ? mState.mFadedSegmentColor : mState.mSegmentColor));
+                mFillPaint.setColor(segment.mColor);
 
                 mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY);
                 canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint);
             } else if (part instanceof Point point) {
-                final float pointWidth = 2 * pointRadius;
-                float start = x - pointRadius;
-                if (start < 0) start = 0;
-                float end = start + pointWidth;
-                if (end > totalWidth) {
-                    end = totalWidth;
-                    if (totalWidth > pointWidth) start = totalWidth - pointWidth;
-                }
-                mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end,
-                        (int) (centerY + pointRadius));
+                // TODO: b/367804171 - actually use a vector asset for the default point
+                //  rather than drawing it as a box?
+                mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius);
+                final float inset = mState.mPointRectInset;
+                final float cornerRadius = mState.mPointRectCornerRadius;
+                mPointRectF.inset(inset, inset);
 
-                if (point.mIcon != null) {
-                    point.mIcon.setBounds(mPointRect);
-                    point.mIcon.draw(canvas);
-                } else {
-                    // TODO: b/367804171 - actually use a vector asset for the default point
-                    //  rather than drawing it as a box?
-                    mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius);
-                    final float inset = mState.mPointRectInset;
-                    final float cornerRadius = mState.mPointRectCornerRadius;
-                    mPointRectF.inset(inset, inset);
+                mFillPaint.setColor(point.mColor);
 
-                    mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor
-                            : (point.mFaded ? mState.mFadedPointRectColor
-                                    : mState.mPointRectColor));
-
-                    canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
-                }
+                canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
             }
         }
     }
 
-    private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap,
-            float startX) {
-        if (!(prevPart instanceof Point)) return 0F;
-        final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0;
-        return pointOffset + pointRadius + segPointGap;
-    }
-
-    private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
-            float segPointGap,
-            float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) {
-        if (nextPart == null) return 0F;
-        if (nextPart instanceof Segment nextSeg) {
-            if (!seg.mFaded && nextSeg.mFaded) {
-                // @see Segment#mFaded
-                return hasTrackerIcon ? 0F : segSegGap;
-            }
-            return segSegGap;
-        }
-
-        final float pointWidth = 2 * pointRadius;
-        final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth)
-                ? (endX + pointRadius - totalWidth) : 0;
-        return segPointGap + pointRadius + pointOffset;
-    }
-
     @Override
     public @Config int getChangingConfigurations() {
         return super.getChangingConfigurations() | mState.getChangingConfigurations();
@@ -260,6 +192,19 @@
         return PixelFormat.UNKNOWN;
     }
 
+    public void setBoundsChangeListener(BoundsChangeListener listener) {
+        mBoundsChangeListener = listener;
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+
+        if (mBoundsChangeListener != null) {
+            mBoundsChangeListener.onDrawableBoundsChanged();
+        }
+    }
+
     @Override
     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
             @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)
@@ -384,6 +329,8 @@
         // Extract the theme attributes, if any.
         state.mThemeAttrsSegments = a.extractThemeAttrs();
 
+        state.mSegmentMinWidth = a.getDimension(
+                R.styleable.NotificationProgressDrawableSegments_minWidth, state.mSegmentMinWidth);
         state.mSegmentHeight = a.getDimension(
                 R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight);
         state.mFadedSegmentHeight = a.getDimension(
@@ -392,9 +339,6 @@
         state.mSegmentCornerRadius = a.getDimension(
                 R.styleable.NotificationProgressDrawableSegments_cornerRadius,
                 state.mSegmentCornerRadius);
-        final int color = a.getColor(R.styleable.NotificationProgressDrawableSegments_color,
-                state.mSegmentColor);
-        setSegmentDefaultColor(color);
     }
 
     private void updatePointsFromTypedArray(TypedArray a) {
@@ -413,9 +357,6 @@
         state.mPointRectCornerRadius = a.getDimension(
                 R.styleable.NotificationProgressDrawablePoints_cornerRadius,
                 state.mPointRectCornerRadius);
-        final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color,
-                state.mPointRectColor);
-        setPointRectDefaultColor(color);
     }
 
     static int resolveDensity(@Nullable Resources r, int parentDensity) {
@@ -464,65 +405,58 @@
     }
 
     /**
-     * A part of the progress bar, which is either a S{@link Segment} with non-zero length, or a
-     * {@link Point} with zero length.
+     * Listener to receive updates about drawable bounds changing
      */
-    public interface Part {
+    public interface BoundsChangeListener {
+        /** Called when bounds have changed */
+        void onDrawableBoundsChanged();
     }
 
     /**
-     * A segment is a part of the progress bar with non-zero length. For example, it can
-     * represent a portion in a navigation journey with certain traffic condition.
-     *
+     * A part of the progress bar, which is either a {@link Segment} with non-zero length and
+     * varying drawing width, or a {@link Point} with zero length and fixed size for drawing.
      */
-    public static final class Segment implements Part {
-        private final float mFraction;
-        @ColorInt private final int mColor;
-        /** Whether the segment is faded or not.
-         * <p>
-         *     <pre>
-         *     When mFaded is set to true, a combination of the following is done to the segment:
-         *       1. The drawing color is mColor with opacity updated to 40%.
-         *       2. The gap between faded and non-faded segments is:
-         *          - the segment-segment gap, when there is no tracker icon
-         *          - 0, when there is tracker icon
-         *     </pre>
-         * </p>
-         */
-        private final boolean mFaded;
+    public abstract static class Part {
+        // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the
+        //  bounds rect.
+        /** Start position for drawing (in pixels) */
+        protected float mStart;
+        /** End position for drawing (in pixels) */
+        protected float mEnd;
+        /** Drawing color. */
+        @ColorInt protected final int mColor;
 
-        public Segment(float fraction) {
-            this(fraction, Color.TRANSPARENT);
-        }
-
-        public Segment(float fraction, @ColorInt int color) {
-            this(fraction, color, false);
-        }
-
-        public Segment(float fraction, @ColorInt int color, boolean faded) {
-            mFraction = fraction;
+        protected Part(float start, float end, @ColorInt int color) {
+            mStart = start;
+            mEnd = end;
             mColor = color;
-            mFaded = faded;
         }
 
-        public float getFraction() {
-            return this.mFraction;
+        public float getStart() {
+            return this.mStart;
+        }
+
+        public void setStart(float start) {
+            mStart = start;
+        }
+
+        public float getEnd() {
+            return this.mEnd;
+        }
+
+        public void setEnd(float end) {
+            mEnd = end;
+        }
+
+        /** Returns the calculated drawing width of the part */
+        public float getWidth() {
+            return mEnd - mStart;
         }
 
         public int getColor() {
             return this.mColor;
         }
 
-        public boolean getFaded() {
-            return this.mFaded;
-        }
-
-        @Override
-        public String toString() {
-            return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded="
-                    + this.mFaded + ')';
-        }
-
         // Needed for unit tests
         @Override
         public boolean equals(@Nullable Object other) {
@@ -530,80 +464,79 @@
 
             if (other == null || getClass() != other.getClass()) return false;
 
-            Segment that = (Segment) other;
-            if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
-            if (this.mColor != that.mColor) return false;
-            return this.mFaded == that.mFaded;
+            Part that = (Part) other;
+            if (Float.compare(this.mStart, that.mStart) != 0) return false;
+            if (Float.compare(this.mEnd, that.mEnd) != 0) return false;
+            return this.mColor == that.mColor;
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(mFraction, mColor, mFaded);
+            return Objects.hash(mStart, mEnd, mColor);
         }
     }
 
     /**
-     * A point is a part of the progress bar with zero length. Points are designated points within a
-     * progressbar to visualize distinct stages or milestones. For example, a stop in a multi-stop
-     * ride-share journey.
+     * A segment is a part of the progress bar with non-zero length. For example, it can
+     * represent a portion in a navigation journey with certain traffic condition.
+     * <p>
+     * The start and end positions for drawing a segment are assumed to have been adjusted for
+     * the Points and gaps neighboring the segment.
+     * </p>
      */
-    public static final class Point implements Part {
-        @Nullable
-        private final Drawable mIcon;
-        @ColorInt private final int mColor;
+    public static final class Segment extends Part {
+        /**
+         * Whether the segment is faded or not.
+         * <p>
+         * Faded segments and non-faded segments are drawn with different heights.
+         * </p>
+         */
         private final boolean mFaded;
 
-        public Point(@Nullable Drawable icon) {
-            this(icon, Color.TRANSPARENT, false);
+        public Segment(float start, float end, int color) {
+            this(start, end, color, false);
         }
 
-        public Point(@Nullable Drawable icon, @ColorInt int color) {
-            this(icon, color, false);
-
-        }
-
-        public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) {
-            mIcon = icon;
-            mColor = color;
+        public Segment(float start, float end, int color, boolean faded) {
+            super(start, end, color);
             mFaded = faded;
         }
 
-        @Nullable
-        public Drawable getIcon() {
-            return this.mIcon;
-        }
-
-        public int getColor() {
-            return this.mColor;
-        }
-
-        public boolean getFaded() {
-            return this.mFaded;
-        }
-
         @Override
         public String toString() {
-            return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded
-                    + ")";
+            return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor
+                    + ", faded=" + this.mFaded + ')';
         }
 
         // Needed for unit tests.
         @Override
         public boolean equals(@Nullable Object other) {
-            if (this == other) return true;
+            if (!super.equals(other)) return false;
 
-            if (other == null || getClass() != other.getClass()) return false;
-
-            Point that = (Point) other;
-
-            if (!Objects.equals(this.mIcon, that.mIcon)) return false;
-            if (this.mColor != that.mColor) return false;
+            Segment that = (Segment) other;
             return this.mFaded == that.mFaded;
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(mIcon, mColor, mFaded);
+            return Objects.hash(super.hashCode(), mFaded);
+        }
+    }
+
+    /**
+     * A point is a part of the progress bar with zero length. Points are designated points within a
+     * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop
+     * ride-share journey.
+     */
+    public static final class Point extends Part {
+        public Point(float start, float end, int color) {
+            super(start, end, color);
+        }
+
+        @Override
+        public String toString() {
+            return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor
+                    + ")";
         }
     }
 
@@ -628,16 +561,14 @@
         int mChangingConfigurations;
         float mSegSegGap = 0.0f;
         float mSegPointGap = 0.0f;
+        float mSegmentMinWidth = 0.0f;
         float mSegmentHeight;
         float mFadedSegmentHeight;
         float mSegmentCornerRadius;
-        int mSegmentColor;
-        int mFadedSegmentColor;
+        // how big the point icon will be, halved
         float mPointRadius;
         float mPointRectInset;
         float mPointRectCornerRadius;
-        int mPointRectColor;
-        int mFadedPointRectColor;
 
         int[] mThemeAttrs;
         int[] mThemeAttrsSegments;
@@ -652,16 +583,13 @@
             mChangingConfigurations = orig.mChangingConfigurations;
             mSegSegGap = orig.mSegSegGap;
             mSegPointGap = orig.mSegPointGap;
+            mSegmentMinWidth = orig.mSegmentMinWidth;
             mSegmentHeight = orig.mSegmentHeight;
             mFadedSegmentHeight = orig.mFadedSegmentHeight;
             mSegmentCornerRadius = orig.mSegmentCornerRadius;
-            mSegmentColor = orig.mSegmentColor;
-            mFadedSegmentColor = orig.mFadedSegmentColor;
             mPointRadius = orig.mPointRadius;
             mPointRectInset = orig.mPointRectInset;
             mPointRectCornerRadius = orig.mPointRectCornerRadius;
-            mPointRectColor = orig.mPointRectColor;
-            mFadedPointRectColor = orig.mFadedPointRectColor;
 
             mThemeAttrs = orig.mThemeAttrs;
             mThemeAttrsSegments = orig.mThemeAttrsSegments;
@@ -674,6 +602,18 @@
         }
 
         private void applyDensityScaling(int sourceDensity, int targetDensity) {
+            if (mSegSegGap > 0) {
+                mSegSegGap = scaleFromDensity(
+                        mSegSegGap, sourceDensity, targetDensity);
+            }
+            if (mSegPointGap > 0) {
+                mSegPointGap = scaleFromDensity(
+                        mSegPointGap, sourceDensity, targetDensity);
+            }
+            if (mSegmentMinWidth > 0) {
+                mSegmentMinWidth = scaleFromDensity(
+                        mSegmentMinWidth, sourceDensity, targetDensity);
+            }
             if (mSegmentHeight > 0) {
                 mSegmentHeight = scaleFromDensity(
                         mSegmentHeight, sourceDensity, targetDensity);
@@ -740,28 +680,6 @@
                 applyDensityScaling(sourceDensity, targetDensity);
             }
         }
-
-        public void setSegmentColor(int color) {
-            mSegmentColor = color;
-            mFadedSegmentColor = getFadedColor(color);
-        }
-
-        public void setPointRectColor(int color) {
-            mPointRectColor = color;
-            mFadedPointRectColor = getFadedColor(color);
-        }
-    }
-
-    /**
-     * Get a color with an opacity that's 25% of the input color.
-     */
-    @ColorInt
-    static int getFadedColor(@ColorInt int color) {
-        return Color.argb(
-                (int) (Color.alpha(color) * 0.4f + 0.5f),
-                Color.red(color),
-                Color.green(color),
-                Color.blue(color));
     }
 
     @Override
diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp
index e6364a9..1e7bfe3 100644
--- a/core/jni/android_content_res_ApkAssets.cpp
+++ b/core/jni/android_content_res_ApkAssets.cpp
@@ -111,8 +111,9 @@
 class LoaderAssetsProvider : public AssetsProvider {
  public:
   static std::unique_ptr<AssetsProvider> Create(JNIEnv* env, jobject assets_provider) {
-      return std::unique_ptr<AssetsProvider>{
-              assets_provider ? new LoaderAssetsProvider(env, assets_provider) : nullptr};
+    return (!assets_provider) ? EmptyAssetsProvider::Create()
+                              : std::unique_ptr<AssetsProvider>(new LoaderAssetsProvider(
+                                    env, assets_provider));
   }
 
   bool ForEachFile(const std::string& /* root_path */,
@@ -128,8 +129,8 @@
     return debug_name_;
   }
 
-  UpToDate IsUpToDate() const override {
-      return UpToDate::Always;
+  bool IsUpToDate() const override {
+    return true;
   }
 
   ~LoaderAssetsProvider() override {
@@ -211,7 +212,7 @@
     auto string_result = static_cast<jstring>(env->CallObjectMethod(
         assets_provider_, gAssetsProviderOffsets.toString));
     ScopedUtfChars str(env, string_result);
-    debug_name_ = std::string(str.c_str());
+    debug_name_ = std::string(str.c_str(), str.size());
   }
 
   // The global reference to the AssetsProvider
@@ -242,10 +243,10 @@
       apk_assets = ApkAssets::LoadOverlay(path.c_str(), property_flags);
       break;
     case FORMAT_ARSC:
-        apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()),
-                                          MultiAssetsProvider::Create(std::move(loader_assets)),
-                                          property_flags);
-        break;
+      apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()),
+                                        std::move(loader_assets),
+                                        property_flags);
+      break;
     case FORMAT_DIRECTORY: {
       auto assets = MultiAssetsProvider::Create(std::move(loader_assets),
                                                 DirectoryAssetsProvider::Create(path.c_str()));
@@ -315,11 +316,10 @@
         break;
     }
     case FORMAT_ARSC:
-        apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd),
-                                                                            nullptr /* path */),
-                                          MultiAssetsProvider::Create(std::move(loader_assets)),
-                                          property_flags);
-        break;
+      apk_assets = ApkAssets::LoadTable(
+          AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */),
+          std::move(loader_assets), property_flags);
+      break;
     default:
       const std::string error_msg = base::StringPrintf("Unsupported format type %d", format);
       jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str());
@@ -386,15 +386,12 @@
         break;
     }
     case FORMAT_ARSC:
-        apk_assets =
-                ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd),
-                                                                       nullptr /* path */,
-                                                                       static_cast<off64_t>(offset),
-                                                                       static_cast<off64_t>(
-                                                                               length)),
-                                     MultiAssetsProvider::Create(std::move(loader_assets)),
-                                     property_flags);
-        break;
+      apk_assets = ApkAssets::LoadTable(
+          AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */,
+                                            static_cast<off64_t>(offset),
+                                            static_cast<off64_t>(length)),
+          std::move(loader_assets), property_flags);
+      break;
     default:
       const std::string error_msg = base::StringPrintf("Unsupported format type %d", format);
       jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str());
@@ -411,16 +408,13 @@
 }
 
 static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jint flags, jobject assets_provider) {
-    auto apk_assets = ApkAssets::Load(MultiAssetsProvider::Create(
-                                              LoaderAssetsProvider::Create(env, assets_provider)),
-                                      flags);
-    if (apk_assets == nullptr) {
-        const std::string error_msg =
-                base::StringPrintf("Failed to load empty assets with provider %p",
-                                   (void*)assets_provider);
-        jniThrowException(env, "java/io/IOException", error_msg.c_str());
-        return 0;
-    }
+  auto apk_assets = ApkAssets::Load(LoaderAssetsProvider::Create(env, assets_provider), flags);
+  if (apk_assets == nullptr) {
+    const std::string error_msg =
+        base::StringPrintf("Failed to load empty assets with provider %p", (void*)assets_provider);
+    jniThrowException(env, "java/io/IOException", error_msg.c_str());
+    return 0;
+  }
   return CreateGuardedApkAssets(std::move(apk_assets));
 }
 
@@ -449,10 +443,10 @@
     return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool());
 }
 
-static jint NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
+static jboolean NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
     auto scoped_apk_assets = ScopedLock(ApkAssetsFromLong(ptr));
     auto apk_assets = scoped_apk_assets->get();
-    return (jint)apk_assets->IsUpToDate();
+    return apk_assets->IsUpToDate() ? JNI_TRUE : JNI_FALSE;
 }
 
 static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) {
@@ -564,7 +558,7 @@
         {"nativeGetDebugName", "(J)Ljava/lang/String;", (void*)NativeGetDebugName},
         {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock},
         // @CriticalNative
-        {"nativeIsUpToDate", "(J)I", (void*)NativeIsUpToDate},
+        {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate},
         {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml},
         {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;",
          (void*)NativeGetOverlayableInfo},
diff --git a/core/jni/android_hardware_UsbDeviceConnection.cpp b/core/jni/android_hardware_UsbDeviceConnection.cpp
index b1221ee..68ef3d4 100644
--- a/core/jni/android_hardware_UsbDeviceConnection.cpp
+++ b/core/jni/android_hardware_UsbDeviceConnection.cpp
@@ -165,19 +165,25 @@
         return -1;
     }
 
-    jbyte* bufferBytes = NULL;
-    if (buffer) {
-        bufferBytes = (jbyte*)env->GetPrimitiveArrayCritical(buffer, NULL);
+    bool is_dir_in = (requestType & USB_ENDPOINT_DIR_MASK) == USB_DIR_IN;
+    std::unique_ptr<jbyte[]> bufferBytes(new (std::nothrow) jbyte[length]);
+    if (!bufferBytes) {
+        jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
+        return -1;
     }
 
-    jint result = usb_device_control_transfer(device, requestType, request,
-            value, index, bufferBytes + start, length, timeout);
-
-    if (bufferBytes) {
-        env->ReleasePrimitiveArrayCritical(buffer, bufferBytes, 0);
+    if (!is_dir_in && buffer) {
+        env->GetByteArrayRegion(buffer, start, length, bufferBytes.get());
     }
 
-    return result;
+    jint bytes_transferred = usb_device_control_transfer(device, requestType, request,
+            value, index, bufferBytes.get(), length, timeout);
+
+    if (bytes_transferred > 0 && is_dir_in) {
+        env->SetByteArrayRegion(buffer, start, bytes_transferred, bufferBytes.get());
+    }
+
+    return bytes_transferred;
 }
 
 static jint
diff --git a/core/res/res/drawable/notification_progress.xml b/core/res/res/drawable/notification_progress.xml
index 5d272fb..ff5450e 100644
--- a/core/res/res/drawable/notification_progress.xml
+++ b/core/res/res/drawable/notification_progress.xml
@@ -24,6 +24,7 @@
             android:segPointGap="@dimen/notification_progress_segPoint_gap">
             <segments
                 android:color="?attr/colorProgressBackgroundNormal"
+                android:minWidth="@dimen/notification_progress_segments_min_width"
                 android:height="@dimen/notification_progress_segments_height"
                 android:fadedHeight="@dimen/notification_progress_segments_faded_height"
                 android:cornerRadius="@dimen/notification_progress_segments_corner_radius"/>
diff --git a/core/res/res/values-round-watch/dimens.xml b/core/res/res/values-round-watch/dimens.xml
index f288b41..59ee554 100644
--- a/core/res/res/values-round-watch/dimens.xml
+++ b/core/res/res/values-round-watch/dimens.xml
@@ -26,6 +26,6 @@
     <item name="input_extract_action_button_height" type="dimen">32dp</item>
     <item name="input_extract_action_icon_padding" type="dimen">5dp</item>
 
-    <item name="global_actions_vertical_padding_percentage" type="fraction">20.8%</item>
+    <item name="global_actions_vertical_padding_percentage" type="fraction">21.8%</item>
     <item name="global_actions_horizontal_padding_percentage" type="fraction">5.2%</item>
 </resources>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 728c856..8372aec 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7572,25 +7572,31 @@
     <!-- NotificationProgressDrawable class -->
     <!-- ================================== -->
 
-    <!-- Drawable used to render a segmented bar, with segments and points. -->
+    <!-- Drawable used to render a notification progress bar, with segments and points. -->
     <!-- @hide internal use only -->
     <declare-styleable name="NotificationProgressDrawable">
-        <!-- Default color for the parts. -->
+        <!-- The gap between two segments. -->
         <attr name="segSegGap" format="dimension" />
+        <!-- The gap between a segment and a point. -->
         <attr name="segPointGap" format="dimension" />
     </declare-styleable>
 
     <!-- Used to config the segments of a NotificationProgressDrawable. -->
     <!-- @hide internal use only -->
     <declare-styleable name="NotificationProgressDrawableSegments">
-        <!-- Height of the solid segments -->
+        <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only
+         place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap
+         above. -->
+        <!-- Minimum required drawing width. The drawing width refers to the width after
+         the original segments have been adjusted for the neighboring Points and gaps. This is
+         enforced by stretching the segments that are too short. -->
+        <attr name="minWidth" format="dimension" />
+        <!-- Height of the solid segments. -->
         <attr name="height" />
-        <!-- Height of the faded segments -->
-        <attr name="fadedHeight" format="dimension"/>
+        <!-- Height of the faded segments. -->
+        <attr name="fadedHeight" format="dimension" />
         <!-- Corner radius of the segment rect. -->
         <attr name="cornerRadius" format="dimension" />
-        <!-- Default color of the segment. -->
-        <attr name="color" />
     </declare-styleable>
 
     <!-- Used to config the points of a NotificationProgressDrawable. -->
@@ -7602,8 +7608,6 @@
         <attr name="inset" />
         <!-- Corner radius of the point rect. -->
         <attr name="cornerRadius"/>
-        <!-- Default color of the point rect. -->
-        <attr name="color" />
     </declare-styleable>
 
     <!-- ========================== -->
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 4f7351c..d6b8704 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -899,6 +899,8 @@
     <dimen name="notification_progress_segSeg_gap">4dp</dimen>
     <!-- The gap between a segment and a point in the notification progress bar -->
     <dimen name="notification_progress_segPoint_gap">4dp</dimen>
+    <!-- The minimum required drawing width of the notification progress bar segments -->
+    <dimen name="notification_progress_segments_min_width">16dp</dimen>
     <!-- The height of the notification progress bar segments -->
     <dimen name="notification_progress_segments_height">6dp</dimen>
     <!-- The height of the notification progress bar faded segments -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f89ca44..6c014e9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3950,6 +3950,7 @@
   <java-symbol type="dimen" name="notification_progress_tracker_height" />
   <java-symbol type="dimen" name="notification_progress_segSeg_gap" />
   <java-symbol type="dimen" name="notification_progress_segPoint_gap" />
+  <java-symbol type="dimen" name="notification_progress_segments_min_width" />
   <java-symbol type="dimen" name="notification_progress_segments_height" />
   <java-symbol type="dimen" name="notification_progress_segments_faded_height" />
   <java-symbol type="dimen" name="notification_progress_segments_corner_radius" />
diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS
index c45080f..5fd4ffc 100644
--- a/core/tests/coretests/src/android/os/OWNERS
+++ b/core/tests/coretests/src/android/os/OWNERS
@@ -10,6 +10,9 @@
 # PerformanceHintManager
 per-file PerformanceHintManagerTest.java = file:/ADPF_OWNERS
 
+# SystemHealthManager
+per-file SystemHealthManagerUnitTest.java = file:/ADPF_OWNERS
+
 # Caching
 per-file IpcDataCache* = file:/PERFORMANCE_OWNERS
 
diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java
index da9d687..3e652010 100644
--- a/core/tests/coretests/src/android/os/ParcelTest.java
+++ b/core/tests/coretests/src/android/os/ParcelTest.java
@@ -361,7 +361,11 @@
 
         p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie");
         p.recycle();
-        assertThat(p.getClassCookie(ParcelTest.class)).isNull();
+
+        // cannot access Parcel after it's recycled!
+        // this test is equivalent to checking hasClassCookie false
+        // after obtaining above
+        // assertThat(p.getClassCookie(ParcelTest.class)).isNull();
     }
 
     @Test
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
index d26bb35..f105ec3 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
@@ -20,12 +20,13 @@
 
 import android.app.Notification.ProgressStyle;
 import android.graphics.Color;
+import android.util.Pair;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
-import com.android.internal.widget.NotificationProgressDrawable.Part;
-import com.android.internal.widget.NotificationProgressDrawable.Point;
-import com.android.internal.widget.NotificationProgressDrawable.Segment;
+import com.android.internal.widget.NotificationProgressBar.Part;
+import com.android.internal.widget.NotificationProgressBar.Point;
+import com.android.internal.widget.NotificationProgressBar.Segment;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,183 +38,303 @@
 public class NotificationProgressBarTest {
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_segmentsIsEmpty() {
+    public void processAndConvertToParts_segmentsIsEmpty() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() {
+    public void processAndConvertToParts_segmentsLengthNotMatchingProgressMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(50));
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_segmentLengthIsNegative() {
+    public void processAndConvertToParts_segmentLengthIsNegative() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(-50));
         segments.add(new ProgressStyle.Segment(150));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_segmentLengthIsZero() {
+    public void processAndConvertToParts_segmentLengthIsZero() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(0));
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_progressIsNegative() {
+    public void processAndConvertToParts_progressIsNegative() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = -50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_progressIsZero() {
+    public void processAndConvertToParts_progressIsZero() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 0;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 300, Color.RED)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 40% opacity
         int fadedRed = 0x66FF0000;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 300, fadedRed, true)));
 
-        List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true)));
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(0);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_progressAtMax() {
+    public void processAndConvertToParts_progressAtMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 100;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 300, Color.RED)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(300);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_progressAboveMax() {
+    public void processAndConvertToParts_progressAboveMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 150;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax, isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_pointPositionIsNegative() {
+    public void processAndConvertToParts_pointPositionIsNegative() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         points.add(new ProgressStyle.Point(-50).setColor(Color.RED));
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_pointPositionAboveMax() {
+    public void processAndConvertToParts_pointPositionAboveMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         points.add(new ProgressStyle.Point(150).setColor(Color.RED));
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() {
+    public void processAndConvertToParts_multipleSegmentsWithoutPoints() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
         segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 60;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(
+                segments, points, progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(
+                new Segment(0.50f, Color.RED),
+                new Segment(0.50f, Color.GREEN)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = true;
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 40% opacity
         int fadedGreen = 0x6600FF00;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 180, Color.GREEN),
+                        new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true)));
 
-        List<Part> expected = new ArrayList<>(List.of(
-                new Segment(0.50f, Color.RED),
-                new Segment(0.10f, Color.GREEN),
-                new Segment(0.40f, fadedGreen, true)));
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(180);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_singleSegmentWithPoints() {
+    public void processAndConvertToParts_multipleSegmentsWithoutPoints_noTracker() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+        segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        int progress = 60;
+        int progressMax = 100;
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(
+                new Segment(0.50f, Color.RED),
+                new Segment(0.50f, Color.GREEN)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = false;
+
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = true;
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        // Colors with 40% opacity
+        int fadedGreen = 0x6600FF00;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 176, Color.GREEN),
+                        new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true)));
+
+        assertThat(p.second).isEqualTo(180);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
+    }
+
+    @Test
+    public void processAndConvertToParts_singleSegmentWithPoints() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
         List<ProgressStyle.Point> points = new ArrayList<>();
@@ -223,31 +344,77 @@
         points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
         int progress = 60;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(
+                new Segment(0.15f, Color.BLUE),
+                new Point(Color.RED),
+                new Segment(0.10f, Color.BLUE),
+                new Point(Color.BLUE),
+                new Segment(0.35f, Color.BLUE),
+                new Point(Color.BLUE),
+                new Segment(0.15f, Color.BLUE),
+                new Point(Color.YELLOW),
+                new Segment(0.25f, Color.BLUE)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 35, Color.BLUE),
+                        new NotificationProgressDrawable.Point(39, 51, Color.RED),
+                        new NotificationProgressDrawable.Segment(55, 65, Color.BLUE),
+                        new NotificationProgressDrawable.Point(69, 81, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(85, 170, Color.BLUE),
+                        new NotificationProgressDrawable.Point(174, 186, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(190, 215, Color.BLUE),
+                        new NotificationProgressDrawable.Point(219, 231, Color.YELLOW),
+                        new NotificationProgressDrawable.Segment(235, 300, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
         // Colors with 40% opacity
         int fadedBlue = 0x660000FF;
         int fadedYellow = 0x66FFFF00;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 34.219177F, Color.BLUE),
+                        new NotificationProgressDrawable.Point(38.219177F, 50.219177F, Color.RED),
+                        new NotificationProgressDrawable.Segment(54.219177F, 70.21918F, Color.BLUE),
+                        new NotificationProgressDrawable.Point(74.21918F, 86.21918F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(90.21918F, 172.38356F, Color.BLUE),
+                        new NotificationProgressDrawable.Point(176.38356F, 188.38356F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(192.38356F, 217.0137F, fadedBlue,
+                                true),
+                        new NotificationProgressDrawable.Point(221.0137F, 233.0137F, fadedYellow),
+                        new NotificationProgressDrawable.Segment(237.0137F, 300F, fadedBlue,
+                                true)));
 
-        List<Part> expected = new ArrayList<>(List.of(
-                new Segment(0.15f, Color.BLUE),
-                new Point(null, Color.RED),
-                new Segment(0.10f, Color.BLUE),
-                new Point(null, Color.BLUE),
-                new Segment(0.35f, Color.BLUE),
-                new Point(null, Color.BLUE),
-                new Segment(0.15f, fadedBlue, true),
-                new Point(null, fadedYellow, true),
-                new Segment(0.25f, fadedBlue, true)));
-
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(182.38356F);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() {
+    public void processAndConvertToParts_multipleSegmentsWithPoints() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
         segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
@@ -258,32 +425,81 @@
         points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
         int progress = 60;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(
+                new Segment(0.15f, Color.RED),
+                new Point(Color.RED),
+                new Segment(0.10f, Color.RED),
+                new Point(Color.BLUE),
+                new Segment(0.25f, Color.RED),
+                new Segment(0.10f, Color.GREEN),
+                new Point(Color.BLUE),
+                new Segment(0.15f, Color.GREEN),
+                new Point(Color.YELLOW),
+                new Segment(0.25f, Color.GREEN)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED),
+                        new NotificationProgressDrawable.Point(39, 51, Color.RED),
+                        new NotificationProgressDrawable.Segment(55, 65, Color.RED),
+                        new NotificationProgressDrawable.Point(69, 81, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(85, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 170, Color.GREEN),
+                        new NotificationProgressDrawable.Point(174, 186, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(190, 215, Color.GREEN),
+                        new NotificationProgressDrawable.Point(219, 231, Color.YELLOW),
+                        new NotificationProgressDrawable.Segment(235, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 40% opacity
         int fadedGreen = 0x6600FF00;
         int fadedYellow = 0x66FFFF00;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 34.095238F, Color.RED),
+                        new NotificationProgressDrawable.Point(38.095238F, 50.095238F, Color.RED),
+                        new NotificationProgressDrawable.Segment(54.095238F, 70.09524F, Color.RED),
+                        new NotificationProgressDrawable.Point(74.09524F, 86.09524F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(90.09524F, 148.9524F, Color.RED),
+                        new NotificationProgressDrawable.Segment(152.95238F, 172.7619F,
+                                Color.GREEN),
+                        new NotificationProgressDrawable.Point(176.7619F, 188.7619F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(192.7619F, 217.33333F,
+                                fadedGreen, true),
+                        new NotificationProgressDrawable.Point(221.33333F, 233.33333F,
+                                fadedYellow),
+                        new NotificationProgressDrawable.Segment(237.33333F, 299.99997F,
+                                fadedGreen, true)));
 
-        List<Part> expected = new ArrayList<>(List.of(
-                new Segment(0.15f, Color.RED),
-                new Point(null, Color.RED),
-                new Segment(0.10f, Color.RED),
-                new Point(null, Color.BLUE),
-                new Segment(0.25f, Color.RED),
-                new Segment(0.10f, Color.GREEN),
-                new Point(null, Color.BLUE),
-                new Segment(0.15f, fadedGreen, true),
-                new Point(null, fadedYellow, true),
-                new Segment(0.25f, fadedGreen, true)));
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(182.7619F);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() {
+    public void processAndConvertToParts_multipleSegmentsWithPoints_notStyledByProgress() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
         segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
@@ -293,21 +509,251 @@
         points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
         int progress = 60;
         int progressMax = 100;
-        boolean isStyledByProgress = false;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
 
-        List<Part> expected = new ArrayList<>(List.of(
+        List<Part> expectedParts = new ArrayList<>(List.of(
                 new Segment(0.15f, Color.RED),
-                new Point(null, Color.RED),
+                new Point(Color.RED),
                 new Segment(0.10f, Color.RED),
-                new Point(null, Color.BLUE),
+                new Point(Color.BLUE),
                 new Segment(0.25f, Color.RED),
                 new Segment(0.25f, Color.GREEN),
-                new Point(null, Color.YELLOW),
+                new Point(Color.YELLOW),
                 new Segment(0.25f, Color.GREEN)));
 
-        assertThat(parts).isEqualTo(expected);
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED),
+                        new NotificationProgressDrawable.Point(39, 51, Color.RED),
+                        new NotificationProgressDrawable.Segment(55, 65, Color.RED),
+                        new NotificationProgressDrawable.Point(69, 81, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(85, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 215, Color.GREEN),
+                        new NotificationProgressDrawable.Point(219, 231, Color.YELLOW),
+                        new NotificationProgressDrawable.Segment(235, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = false;
+
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 34.296295F, Color.RED),
+                        new NotificationProgressDrawable.Point(38.296295F, 50.296295F, Color.RED),
+                        new NotificationProgressDrawable.Segment(54.296295F, 70.296295F, Color.RED),
+                        new NotificationProgressDrawable.Point(74.296295F, 86.296295F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(90.296295F, 149.62962F, Color.RED),
+                        new NotificationProgressDrawable.Segment(153.62962F, 216.8148F,
+                                Color.GREEN),
+                        new NotificationProgressDrawable.Point(220.81482F, 232.81482F,
+                                Color.YELLOW),
+                        new NotificationProgressDrawable.Segment(236.81482F, 300, Color.GREEN)));
+
+        assertThat(p.second).isEqualTo(182.9037F);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
+    }
+
+    // The only difference from the `zeroWidthDrawableSegment` test below is the longer
+    // segmentMinWidth (= 16dp).
+    @Test
+    public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        int progress = 1000;
+        int progressMax = 1000;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(
+                segments, points, progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(
+                new Point(Color.BLUE),
+                new Segment(0.1f, Color.BLUE),
+                new Segment(0.2f, Color.BLUE),
+                new Segment(0.3f, Color.BLUE),
+                new Segment(0.4f, Color.BLUE)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 200;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 16, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(20, 56, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(60, 116, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = true;
+
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 200,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 32, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(36, 69.41936F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(73.41936F, 124.25807F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(128.25807F, 200, Color.BLUE)));
+
+        assertThat(p.second).isEqualTo(200);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
+    }
+
+    // The only difference from the `negativeWidthDrawableSegment` test above is the shorter
+    // segmentMinWidth (= 10dp).
+    @Test
+    public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        int progress = 1000;
+        int progressMax = 1000;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(
+                segments, points, progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(
+                new Point(Color.BLUE),
+                new Segment(0.1f, Color.BLUE),
+                new Segment(0.2f, Color.BLUE),
+                new Segment(0.3f, Color.BLUE),
+                new Segment(0.4f, Color.BLUE)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 200;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 16, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(20, 56, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(60, 116, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 10;
+        boolean isStyledByProgress = true;
+
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 200,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 26, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(30, 64.169014F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(68.169014F, 120.92958F,
+                                Color.BLUE),
+                        new NotificationProgressDrawable.Segment(124.92958F, 200, Color.BLUE)));
+
+        assertThat(p.second).isEqualTo(200);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
+    }
+
+    @Test
+    public void maybeStretchAndRescaleSegments_noStretchingNecessary() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        int progress = 1000;
+        int progressMax = 1000;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(
+                segments, points, progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(
+                new Point(Color.BLUE),
+                new Segment(0.2f, Color.BLUE),
+                new Segment(0.1f, Color.BLUE),
+                new Segment(0.3f, Color.BLUE),
+                new Segment(0.4f, Color.BLUE)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 200;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 36, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(40, 56, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(60, 116, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 10;
+        boolean isStyledByProgress = true;
+
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 200,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        assertThat(p.second).isEqualTo(200);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 }
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 50c95a9..3378cc1 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -16,9 +16,9 @@
 
 package android.graphics;
 
+import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API;
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
 import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
-import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API;
 import static com.android.text.flags.Flags.FLAG_VERTICAL_TEXT_LAYOUT;
 
 import android.annotation.ColorInt;
@@ -34,7 +34,6 @@
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.graphics.fonts.FontStyle;
 import android.graphics.fonts.FontVariationAxis;
 import android.graphics.text.TextRunShaper;
 import android.os.Build;
@@ -2100,14 +2099,6 @@
     }
 
     /**
-     * A change ID for new font variation settings management.
-     * @hide
-     */
-    @ChangeId
-    @EnabledSince(targetSdkVersion = 36)
-    public static final long NEW_FONT_VARIATION_MANAGEMENT = 361260253L;
-
-    /**
      * Sets TrueType or OpenType font variation settings. The settings string is constructed from
      * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
      * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
@@ -2136,16 +2127,12 @@
      * </li>
      * </ul>
      *
-     * <p>Note: If the application that targets API 35 or before, this function mutates the
-     * underlying typeface instance.
-     *
      * @param fontVariationSettings font variation settings. You can pass null or empty string as
      *                              no variation settings.
      *
-     * @return If the application that targets API 36 or later and is running on devices API 36 or
-     *         later, this function always returns true. Otherwise, this function returns true if
-     *         the given settings is effective to at least one font file underlying this typeface.
-     *         This function also returns true for empty settings string. Otherwise returns false.
+     * @return true if the given settings is effective to at least one font file underlying this
+     *         typeface. This function also returns true for empty settings string. Otherwise
+     *         returns false
      *
      * @throws IllegalArgumentException If given string is not a valid font variation settings
      *                                  format
@@ -2154,39 +2141,6 @@
      * @see FontVariationAxis
      */
     public boolean setFontVariationSettings(String fontVariationSettings) {
-        return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */);
-    }
-
-    /**
-     * Set font variation settings with weight adjustment
-     * @hide
-     */
-    public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) {
-        final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
-                && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
-        if (useFontVariationStore) {
-            FontVariationAxis[] axes =
-                    FontVariationAxis.fromFontVariationSettings(fontVariationSettings);
-            if (axes == null) {
-                nSetFontVariationOverride(mNativePaint, 0);
-                mFontVariationSettings = null;
-                return true;
-            }
-
-            long builderPtr = nCreateFontVariationBuilder(axes.length);
-            for (int i = 0; i < axes.length; ++i) {
-                int tag = axes[i].getOpenTypeTagValue();
-                float value = axes[i].getStyleValue();
-                if (tag == 0x77676874 /* wght */) {
-                    value = Math.clamp(value + wghtAdjust,
-                            FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX);
-                }
-                nAddFontVariationToBuilder(builderPtr, tag, value);
-            }
-            nSetFontVariationOverride(mNativePaint, builderPtr);
-            mFontVariationSettings = fontVariationSettings;
-            return true;
-        }
         final String settings = TextUtils.nullIfEmpty(fontVariationSettings);
         if (settings == mFontVariationSettings
                 || (settings != null && settings.equals(mFontVariationSettings))) {
diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java
index d1fe2cd..30a248b 100644
--- a/graphics/java/android/graphics/fonts/FontVariationAxis.java
+++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java
@@ -23,6 +23,7 @@
 import android.text.TextUtils;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.regex.Pattern;
@@ -139,9 +140,19 @@
      */
     public static @Nullable FontVariationAxis[] fromFontVariationSettings(
             @Nullable String settings) {
-        if (settings == null || settings.isEmpty()) {
+        List<FontVariationAxis> result = fromFontVariationSettingsForList(settings);
+        if (result.isEmpty()) {
             return null;
         }
+        return result.toArray(new FontVariationAxis[0]);
+    }
+
+    /** @hide */
+    public static @NonNull List<FontVariationAxis> fromFontVariationSettingsForList(
+            @Nullable String settings) {
+        if (settings == null || settings.isEmpty()) {
+            return Collections.emptyList();
+        }
         final ArrayList<FontVariationAxis> axisList = new ArrayList<>();
         final int length = settings.length();
         for (int i = 0; i < length; i++) {
@@ -172,9 +183,9 @@
             i = endOfValueString;
         }
         if (axisList.isEmpty()) {
-            return null;
+            return Collections.emptyList();
         }
-        return axisList.toArray(new FontVariationAxis[0]);
+        return axisList;
     }
 
     /**
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 755f472..2fed138 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -233,6 +233,16 @@
     }
 
     /**
+     * Returns whether the multiple desktops feature is enabled for this device (both backend and
+     * frontend implementations).
+     */
+    public static boolean enableMultipleDesktops(@NonNull Context context) {
+        return Flags.enableMultipleDesktopsBackend()
+                && Flags.enableMultipleDesktopsFrontend()
+                && canEnterDesktopMode(context);
+    }
+
+    /**
      * @return {@code true} if this device is requesting to show the app handle despite non
      * necessarily enabling desktop mode
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java
new file mode 100644
index 0000000..5018fdb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.animation;
+
+import static com.android.wm.shell.transition.DefaultSurfaceAnimator.setupValueAnimator;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.Transformation;
+import android.view.animation.TranslateAnimation;
+
+import java.util.function.Consumer;
+
+/**
+ * Animation implementation for size-changing window container animations. Ported from
+ * {@link com.android.server.wm.WindowChangeAnimationSpec}.
+ * <p>
+ * This animation behaves slightly differently depending on whether the window is growing
+ * or shrinking:
+ * <ul>
+ * <li>If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old)
+ * snapshot.
+ * <li>If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker
+ * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into
+ * place.
+ * </ul>
+ */
+public class SizeChangeAnimation {
+    private final Rect mTmpRect = new Rect();
+    final Transformation mTmpTransform = new Transformation();
+    final Matrix mTmpMatrix = new Matrix();
+    final float[] mTmpFloats = new float[9];
+    final float[] mTmpVecs = new float[4];
+
+    private final Animation mAnimation;
+    private final Animation mSnapshotAnim;
+
+    private final ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f);
+
+    /**
+     * The maximum of stretching applied to any surface during interpolation (since the animation
+     * is a combination of stretching/cropping/fading).
+     */
+    private static final float SCALE_FACTOR = 0.7f;
+
+    /**
+     * Since this animation is made of several sub-animations, we want to pre-arrange the
+     * sub-animations on a "virtual timeline" and then drive the overall progress in lock-step.
+     *
+     * To do this, we have a single value-animator which animates progress from 0-1 with an
+     * arbitrary duration and interpolator. Then we convert the progress to a frame in our virtual
+     * timeline to get the interpolated transforms.
+     *
+     * The APIs for arranging the sub-animations use integral frame numbers, so we need to pick
+     * an integral "duration" for our virtual timeline. That's what this constant specifies. It
+     * is effectively an animation "resolution" since it divides-up the 0-1 interpolation-space.
+     */
+    private static final int ANIMATION_RESOLUTION = 1000;
+
+    public SizeChangeAnimation(Rect startBounds, Rect endBounds) {
+        mAnimation = buildContainerAnimation(startBounds, endBounds);
+        mSnapshotAnim = buildSnapshotAnimation(startBounds, endBounds);
+    }
+
+    /**
+     * Initialize a size-change animation for a container leash.
+     */
+    public void initialize(SurfaceControl leash, SurfaceControl snapshot,
+            SurfaceControl.Transaction startT) {
+        startT.reparent(snapshot, leash);
+        startT.setPosition(snapshot, 0, 0);
+        startT.show(snapshot);
+        startT.show(leash);
+        apply(startT, leash, snapshot, 0.f);
+    }
+
+    /**
+     * Initialize a size-change animation for a view containing the leash surface(s).
+     *
+     * Note that this **will** apply {@param startToApply}!
+     */
+    public void initialize(View view, SurfaceControl leash, SurfaceControl snapshot,
+            SurfaceControl.Transaction startToApply) {
+        startToApply.reparent(snapshot, leash);
+        startToApply.setPosition(snapshot, 0, 0);
+        startToApply.show(snapshot);
+        startToApply.show(leash);
+        apply(view, startToApply, leash, snapshot, 0.f);
+    }
+
+    private ValueAnimator buildAnimatorInner(ValueAnimator.AnimatorUpdateListener updater,
+            SurfaceControl leash, SurfaceControl snapshot, Consumer<Animator> onFinish,
+            SurfaceControl.Transaction transaction, @Nullable View view) {
+        return setupValueAnimator(mAnimator, updater, (anim) -> {
+            transaction.reparent(snapshot, null);
+            if (view != null) {
+                view.setClipBounds(null);
+                view.setAnimationMatrix(null);
+                transaction.setCrop(leash, null);
+            }
+            transaction.apply();
+            transaction.close();
+            onFinish.accept(anim);
+        });
+    }
+
+    /**
+     * Build an animator which works on a pair of surface controls (where the snapshot is assumed
+     * to be a child of the main leash).
+     *
+     * @param onFinish Called when animation finishes. This is called on the anim thread!
+     */
+    public ValueAnimator buildAnimator(SurfaceControl leash, SurfaceControl snapshot,
+            Consumer<Animator> onFinish) {
+        final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+        Choreographer choreographer = Choreographer.getInstance();
+        return buildAnimatorInner(animator -> {
+            // The finish callback in buildSurfaceAnimation will ensure that the animation ends
+            // with fraction 1.
+            final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f);
+            apply(transaction, leash, snapshot, progress);
+            transaction.setFrameTimelineVsync(choreographer.getVsyncId());
+            transaction.apply();
+        }, leash, snapshot, onFinish, transaction, null /* view */);
+    }
+
+    /**
+     * Build an animator which works on a view that contains a pair of surface controls (where
+     * the snapshot is assumed to be a child of the main leash).
+     *
+     * @param onFinish Called when animation finishes. This is called on the anim thread!
+     */
+    public ValueAnimator buildViewAnimator(View view, SurfaceControl leash,
+            SurfaceControl snapshot, Consumer<Animator> onFinish) {
+        final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+        return buildAnimatorInner(animator -> {
+            // The finish callback in buildSurfaceAnimation will ensure that the animation ends
+            // with fraction 1.
+            final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f);
+            apply(view, transaction, leash, snapshot, progress);
+        }, leash, snapshot, onFinish, transaction, view);
+    }
+
+    /** Animation for the whole container (snapshot is inside this container). */
+    private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds) {
+        final long duration = ANIMATION_RESOLUTION;
+        boolean growing = endBounds.width() - startBounds.width()
+                + endBounds.height() - startBounds.height() >= 0;
+        long scalePeriod = (long) (duration * SCALE_FACTOR);
+        float startScaleX = SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width()
+                + (1.f - SCALE_FACTOR);
+        float startScaleY = SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height()
+                + (1.f - SCALE_FACTOR);
+        final AnimationSet animSet = new AnimationSet(true);
+
+        final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1);
+        scaleAnim.setDuration(scalePeriod);
+        if (!growing) {
+            scaleAnim.setStartOffset(duration - scalePeriod);
+        }
+        animSet.addAnimation(scaleAnim);
+        final Animation translateAnim = new TranslateAnimation(startBounds.left,
+                endBounds.left, startBounds.top, endBounds.top);
+        translateAnim.setDuration(duration);
+        animSet.addAnimation(translateAnim);
+        Rect startClip = new Rect(startBounds);
+        Rect endClip = new Rect(endBounds);
+        startClip.offsetTo(0, 0);
+        endClip.offsetTo(0, 0);
+        final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+        clipAnim.setDuration(duration);
+        animSet.addAnimation(clipAnim);
+        animSet.initialize(startBounds.width(), startBounds.height(),
+                endBounds.width(), endBounds.height());
+        return animSet;
+    }
+
+    /** The snapshot surface is assumed to be a child of the container surface. */
+    private static AnimationSet buildSnapshotAnimation(Rect startBounds, Rect endBounds) {
+        final long duration = ANIMATION_RESOLUTION;
+        boolean growing = endBounds.width() - startBounds.width()
+                + endBounds.height() - startBounds.height() >= 0;
+        long scalePeriod = (long) (duration * SCALE_FACTOR);
+        float endScaleX = 1.f / (SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width()
+                + (1.f - SCALE_FACTOR));
+        float endScaleY = 1.f / (SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height()
+                + (1.f - SCALE_FACTOR));
+
+        AnimationSet snapAnimSet = new AnimationSet(true);
+        // Animation for the "old-state" snapshot that is atop the task.
+        final Animation snapAlphaAnim = new AlphaAnimation(1.f, 0.f);
+        snapAlphaAnim.setDuration(scalePeriod);
+        if (!growing) {
+            snapAlphaAnim.setStartOffset(duration - scalePeriod);
+        }
+        snapAnimSet.addAnimation(snapAlphaAnim);
+        final Animation snapScaleAnim =
+                new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY);
+        snapScaleAnim.setDuration(duration);
+        snapAnimSet.addAnimation(snapScaleAnim);
+        snapAnimSet.initialize(startBounds.width(), startBounds.height(),
+                endBounds.width(), endBounds.height());
+        return snapAnimSet;
+    }
+
+    private void calcCurrentClipBounds(Rect outClip, Transformation fromTransform) {
+        // The following applies an inverse scale to the clip-rect so that it crops "after" the
+        // scale instead of before.
+        mTmpVecs[1] = mTmpVecs[2] = 0;
+        mTmpVecs[0] = mTmpVecs[3] = 1;
+        fromTransform.getMatrix().mapVectors(mTmpVecs);
+
+        mTmpVecs[0] = 1.f / mTmpVecs[0];
+        mTmpVecs[3] = 1.f / mTmpVecs[3];
+        final Rect clipRect = fromTransform.getClipRect();
+        outClip.left = (int) (clipRect.left * mTmpVecs[0] + 0.5f);
+        outClip.right = (int) (clipRect.right * mTmpVecs[0] + 0.5f);
+        outClip.top = (int) (clipRect.top * mTmpVecs[3] + 0.5f);
+        outClip.bottom = (int) (clipRect.bottom * mTmpVecs[3] + 0.5f);
+    }
+
+    private void apply(SurfaceControl.Transaction t, SurfaceControl leash, SurfaceControl snapshot,
+            float progress) {
+        long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress);
+        // update thumbnail surface
+        mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform);
+        t.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats);
+        t.setAlpha(snapshot, mTmpTransform.getAlpha());
+
+        // update container surface
+        mAnimation.getTransformation(currentPlayTime, mTmpTransform);
+        final Matrix matrix = mTmpTransform.getMatrix();
+        t.setMatrix(leash, matrix, mTmpFloats);
+
+        calcCurrentClipBounds(mTmpRect, mTmpTransform);
+        t.setCrop(leash, mTmpRect);
+    }
+
+    private void apply(View view, SurfaceControl.Transaction tmpT, SurfaceControl leash,
+            SurfaceControl snapshot, float progress) {
+        long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress);
+        // update thumbnail surface
+        mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform);
+        tmpT.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats);
+        tmpT.setAlpha(snapshot, mTmpTransform.getAlpha());
+
+        // update container surface
+        mAnimation.getTransformation(currentPlayTime, mTmpTransform);
+        final Matrix matrix = mTmpTransform.getMatrix();
+        mTmpMatrix.set(matrix);
+        // animationMatrix is applied after getTranslation, so "move" the translate to the end.
+        mTmpMatrix.preTranslate(-view.getTranslationX(), -view.getTranslationY());
+        mTmpMatrix.postTranslate(view.getTranslationX(), view.getTranslationY());
+        view.setAnimationMatrix(mTmpMatrix);
+
+        calcCurrentClipBounds(mTmpRect, mTmpTransform);
+        tmpT.setCrop(leash, mTmpRect);
+        view.setClipBounds(mTmpRect);
+
+        // this takes stuff out of mTmpT so mTmpT can be re-used immediately
+        view.getViewRootImpl().applyTransactionOnDraw(tmpT);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java
new file mode 100644
index 0000000..9451374
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java
@@ -0,0 +1,33 @@
+/*
+ * 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.wm.shell.appzoomout;
+
+import com.android.wm.shell.shared.annotations.ExternalThread;
+
+/**
+ * Interface to engage with the app zoom out feature.
+ */
+@ExternalThread
+public interface AppZoomOut {
+
+    /**
+     * Called when the zoom out progress is updated, which is used to scale down the current app
+     * surface from fullscreen to the max pushback level we want to apply. {@param progress} ranges
+     * between [0,1], 0 when fullscreen, 1 when it's at the max pushback level.
+     */
+    void setProgress(float progress);
+}
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
new file mode 100644
index 0000000..8cd7b0f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
@@ -0,0 +1,158 @@
+/*
+ * 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.wm.shell.appzoomout;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.Slog;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.shared.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
+
+/** Class that manages the app zoom out UI and states. */
+public class AppZoomOutController implements RemoteCallable<AppZoomOutController>,
+        ShellTaskOrganizer.FocusListener, DisplayChangeController.OnDisplayChangingListener {
+
+    private static final String TAG = "AppZoomOutController";
+
+    private final Context mContext;
+    private final ShellTaskOrganizer mTaskOrganizer;
+    private final DisplayController mDisplayController;
+    private final AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer;
+    private final ShellExecutor mMainExecutor;
+    private final AppZoomOutImpl mImpl = new AppZoomOutImpl();
+
+    private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener =
+            new DisplayController.OnDisplaysChangedListener() {
+                @Override
+                public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+                    if (displayId != DEFAULT_DISPLAY) {
+                        return;
+                    }
+                    updateDisplayLayout(displayId);
+                }
+
+                @Override
+                public void onDisplayAdded(int displayId) {
+                    if (displayId != DEFAULT_DISPLAY) {
+                        return;
+                    }
+                    updateDisplayLayout(displayId);
+                }
+            };
+
+
+    public static AppZoomOutController create(Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController,
+            DisplayLayout displayLayout, @ShellMainThread ShellExecutor mainExecutor) {
+        AppZoomOutDisplayAreaOrganizer displayAreaOrganizer = new AppZoomOutDisplayAreaOrganizer(
+                context, displayLayout, mainExecutor);
+        return new AppZoomOutController(context, shellInit, shellTaskOrganizer, displayController,
+                displayAreaOrganizer, mainExecutor);
+    }
+
+    @VisibleForTesting
+    AppZoomOutController(Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController,
+            AppZoomOutDisplayAreaOrganizer displayAreaOrganizer,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        mContext = context;
+        mTaskOrganizer = shellTaskOrganizer;
+        mDisplayController = displayController;
+        mDisplayAreaOrganizer = displayAreaOrganizer;
+        mMainExecutor = mainExecutor;
+
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mTaskOrganizer.addFocusListener(this);
+
+        mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
+        mDisplayController.addDisplayChangingController(this);
+
+        mDisplayAreaOrganizer.registerOrganizer();
+    }
+
+    public AppZoomOut asAppZoomOut() {
+        return mImpl;
+    }
+
+    public void setProgress(float progress) {
+        mDisplayAreaOrganizer.setProgress(progress);
+    }
+
+    void updateDisplayLayout(int displayId) {
+        final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId);
+        if (newDisplayLayout == null) {
+            Slog.w(TAG, "Failed to get new DisplayLayout.");
+            return;
+        }
+        mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout);
+    }
+
+    @Override
+    public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        if (taskInfo == null) {
+            return;
+        }
+        if (taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME) {
+            mDisplayAreaOrganizer.setIsHomeTaskFocused(taskInfo.isFocused);
+        }
+    }
+
+    @Override
+    public void onDisplayChange(int displayId, int fromRotation, int toRotation,
+            @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
+        // TODO: verify if there is synchronization issues.
+        mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation);
+    }
+
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public ShellExecutor getRemoteCallExecutor() {
+        return mMainExecutor;
+    }
+
+    @ExternalThread
+    private class AppZoomOutImpl implements AppZoomOut {
+        @Override
+        public void setProgress(float progress) {
+            mMainExecutor.execute(() -> AppZoomOutController.this.setProgress(progress));
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java
new file mode 100644
index 0000000..1c37461
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java
@@ -0,0 +1,157 @@
+/*
+ * 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.wm.shell.appzoomout;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaAppearedInfo;
+import android.window.DisplayAreaInfo;
+import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerToken;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayLayout;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/** Display area organizer that manages the app zoom out UI and states. */
+public class AppZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer {
+
+    private static final float PUSHBACK_SCALE_FOR_LAUNCHER = 0.05f;
+    private static final float PUSHBACK_SCALE_FOR_APP = 0.025f;
+    private static final float INVALID_PROGRESS = -1;
+
+    private final DisplayLayout mDisplayLayout = new DisplayLayout();
+    private final Context mContext;
+    private final float mCornerRadius;
+    private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap =
+            new ArrayMap<>();
+
+    private float mProgress = INVALID_PROGRESS;
+    // Denote whether the home task is focused, null when it's not yet initialized.
+    @Nullable private Boolean mIsHomeTaskFocused;
+
+    public AppZoomOutDisplayAreaOrganizer(Context context,
+            DisplayLayout displayLayout, Executor mainExecutor) {
+        super(mainExecutor);
+        mContext = context;
+        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        setDisplayLayout(displayLayout);
+    }
+
+    @Override
+    public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) {
+        leash.setUnreleasedWarningCallSite(
+                "AppZoomOutDisplayAreaOrganizer.onDisplayAreaAppeared");
+        mDisplayAreaTokenMap.put(displayAreaInfo.token, leash);
+    }
+
+    @Override
+    public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) {
+        final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token);
+        if (leash != null) {
+            leash.release();
+        }
+        mDisplayAreaTokenMap.remove(displayAreaInfo.token);
+    }
+
+    public void registerOrganizer() {
+        final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer(
+                AppZoomOutDisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT);
+        for (int i = 0; i < displayAreaInfos.size(); i++) {
+            final DisplayAreaAppearedInfo info = displayAreaInfos.get(i);
+            onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash());
+        }
+    }
+
+    @Override
+    public void unregisterOrganizer() {
+        super.unregisterOrganizer();
+        reset();
+    }
+
+    void setProgress(float progress) {
+        if (mProgress == progress) {
+            return;
+        }
+
+        mProgress = progress;
+        apply();
+    }
+
+    void setIsHomeTaskFocused(boolean isHomeTaskFocused) {
+        if (mIsHomeTaskFocused != null && mIsHomeTaskFocused == isHomeTaskFocused) {
+            return;
+        }
+
+        mIsHomeTaskFocused = isHomeTaskFocused;
+        apply();
+    }
+
+    private void apply() {
+        if (mIsHomeTaskFocused == null || mProgress == INVALID_PROGRESS) {
+            return;
+        }
+
+        SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+        float scale = mProgress * (mIsHomeTaskFocused
+                ? PUSHBACK_SCALE_FOR_LAUNCHER : PUSHBACK_SCALE_FOR_APP);
+        mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, scale));
+        tx.apply();
+    }
+
+    void setDisplayLayout(DisplayLayout displayLayout) {
+        mDisplayLayout.set(displayLayout);
+    }
+
+    private void reset() {
+        setProgress(0);
+        mProgress = INVALID_PROGRESS;
+        mIsHomeTaskFocused = null;
+    }
+
+    private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) {
+        if (scale == 0) {
+            // Reset when scale is set back to 0.
+            tx
+                    .setCrop(leash, null)
+                    .setScale(leash, 1, 1)
+                    .setPosition(leash, 0, 0)
+                    .setCornerRadius(leash, 0);
+            return;
+        }
+
+        tx
+                // Rounded corner can only be applied if a crop is set.
+                .setCrop(leash, 0, 0, mDisplayLayout.width(), mDisplayLayout.height())
+                .setScale(leash, 1 - scale, 1 - scale)
+                .setPosition(leash, scale * mDisplayLayout.width() * 0.5f,
+                        scale * mDisplayLayout.height() * 0.5f)
+                .setCornerRadius(leash, mCornerRadius * (1 - scale));
+    }
+
+    void onRotateDisplay(Context context, int toRotation) {
+        if (mDisplayLayout.rotation() == toRotation) {
+            return;
+        }
+        mDisplayLayout.rotateTo(context.getResources(), toRotation);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
index c493aad..151dc43 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.wm.shell.appzoomout.AppZoomOut;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.desktopmode.DesktopMode;
@@ -112,4 +113,7 @@
      */
     @WMSingleton
     Optional<DesktopMode> getDesktopMode();
+
+    @WMSingleton
+    Optional<AppZoomOut> getAppZoomOut();
 }
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 23a0f4a..ab3c33e 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
@@ -91,6 +91,7 @@
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
 import com.android.wm.shell.freeform.FreeformComponents;
@@ -111,6 +112,8 @@
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.appzoomout.AppZoomOut;
+import com.android.wm.shell.appzoomout.AppZoomOutController;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.startingsurface.StartingSurface;
@@ -1031,6 +1034,38 @@
         });
     }
 
+    @WMSingleton
+    @Provides
+    static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() {
+        return new DesktopWallpaperActivityTokenProvider();
+    }
+
+    @WMSingleton
+    @Provides
+    static Optional<DesktopWallpaperActivityTokenProvider>
+            provideOptionalDesktopWallpaperActivityTokenProvider(
+            Context context,
+            DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider) {
+        if (DesktopModeStatus.canEnterDesktopMode(context)) {
+            return Optional.of(desktopWallpaperActivityTokenProvider);
+        }
+        return Optional.empty();
+    }
+
+    //
+    // App zoom out (optional feature)
+    //
+
+    @WMSingleton
+    @Provides
+    static Optional<AppZoomOut> provideAppZoomOut(
+            Optional<AppZoomOutController> appZoomOutController) {
+        return appZoomOutController.map((controller) -> controller.asAppZoomOut());
+    }
+
+    @BindsOptionalOf
+    abstract AppZoomOutController optionalAppZoomOutController();
+
     //
     // Task Stack
     //
@@ -1075,6 +1110,7 @@
             Optional<RecentTasksController> recentTasksOptional,
             Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional,
             Optional<OneHandedController> oneHandedControllerOptional,
+            Optional<AppZoomOutController> appZoomOutControllerOptional,
             Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
             Optional<ActivityEmbeddingController> activityEmbeddingOptional,
             Optional<MixedTransitionHandler> mixedTransitionHandler,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 1916215..e8add56 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -50,6 +50,7 @@
 import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
 import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
 import com.android.wm.shell.apptoweb.AssistContentRequester;
+import com.android.wm.shell.appzoomout.AppZoomOutController;
 import com.android.wm.shell.back.BackAnimationController;
 import com.android.wm.shell.bubbles.BubbleController;
 import com.android.wm.shell.bubbles.BubbleData;
@@ -945,7 +946,8 @@
             FocusTransitionObserver focusTransitionObserver,
             DesktopModeEventLogger desktopModeEventLogger,
             DesktopModeUiEventLogger desktopModeUiEventLogger,
-            WindowDecorTaskResourceLoader taskResourceLoader
+            WindowDecorTaskResourceLoader taskResourceLoader,
+            RecentsTransitionHandler recentsTransitionHandler
     ) {
         if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
             return Optional.empty();
@@ -961,7 +963,7 @@
                 desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
                 windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
                 focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
-                taskResourceLoader));
+                taskResourceLoader, recentsTransitionHandler));
     }
 
     @WMSingleton
@@ -1312,10 +1314,21 @@
         return new DesktopModeUiEventLogger(uiEventLogger, packageManager);
     }
 
+    //
+    // App zoom out
+    //
+
     @WMSingleton
     @Provides
-    static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() {
-        return new DesktopWallpaperActivityTokenProvider();
+    static AppZoomOutController provideAppZoomOutController(
+            Context context,
+            ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            DisplayController displayController,
+            DisplayLayout displayLayout,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        return AppZoomOutController.create(context, shellInit, shellTaskOrganizer,
+                displayController, displayLayout, mainExecutor);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 9e2b9b2..c8d0dab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -41,6 +41,7 @@
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.pip2.phone.PhonePipMenuController;
 import com.android.wm.shell.pip2.phone.PipController;
 import com.android.wm.shell.pip2.phone.PipMotionHelper;
@@ -82,11 +83,14 @@
             @NonNull PipTransitionState pipStackListenerController,
             @NonNull PipDisplayLayoutState pipDisplayLayoutState,
             @NonNull PipUiStateChangeController pipUiStateChangeController,
-            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) {
+            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+            Optional<DesktopWallpaperActivityTokenProvider>
+                    desktopWallpaperActivityTokenProviderOptional) {
         return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
                 pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
                 pipScheduler, pipStackListenerController, pipDisplayLayoutState,
-                pipUiStateChangeController, desktopUserRepositoriesOptional);
+                pipUiStateChangeController, desktopUserRepositoriesOptional,
+                desktopWallpaperActivityTokenProviderOptional);
     }
 
     @WMSingleton
@@ -138,9 +142,12 @@
             @ShellMainThread ShellExecutor mainExecutor,
             PipTransitionState pipTransitionState,
             Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+            Optional<DesktopWallpaperActivityTokenProvider>
+                    desktopWallpaperActivityTokenProviderOptional,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
         return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState,
-                desktopUserRepositoriesOptional, rootTaskDisplayAreaOrganizer);
+                desktopUserRepositoriesOptional, desktopWallpaperActivityTokenProviderOptional,
+                rootTaskDisplayAreaOrganizer);
     }
 
     @WMSingleton
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 d306664..1a58363 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
@@ -27,6 +27,7 @@
 import androidx.core.util.keyIterator
 import androidx.core.util.valueIterator
 import com.android.internal.protolog.ProtoLog
+import com.android.window.flags.Flags
 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -56,6 +57,10 @@
      * @property topTransparentFullscreenTaskId the task id of any current top transparent
      *   fullscreen task launched on top of Desktop Mode. Cleared when the transparent task is
      *   closed or sent to back. (top is at index 0).
+     * @property pipTaskId the task id of PiP task entered while in Desktop Mode.
+     * @property pipShouldKeepDesktopActive whether an active PiP window should keep the Desktop
+     *   Mode session active. Only false when we are explicitly exiting Desktop Mode (via user
+     *   action) while there is an active PiP window.
      */
     private data class DesktopTaskData(
         val activeTasks: ArraySet<Int> = ArraySet(),
@@ -66,6 +71,8 @@
         val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
         var fullImmersiveTaskId: Int? = null,
         var topTransparentFullscreenTaskId: Int? = null,
+        var pipTaskId: Int? = null,
+        var pipShouldKeepDesktopActive: Boolean = true,
     ) {
         fun deepCopy(): DesktopTaskData =
             DesktopTaskData(
@@ -76,6 +83,8 @@
                 freeformTasksInZOrder = ArrayList(freeformTasksInZOrder),
                 fullImmersiveTaskId = fullImmersiveTaskId,
                 topTransparentFullscreenTaskId = topTransparentFullscreenTaskId,
+                pipTaskId = pipTaskId,
+                pipShouldKeepDesktopActive = pipShouldKeepDesktopActive,
             )
 
         fun clear() {
@@ -86,6 +95,8 @@
             freeformTasksInZOrder.clear()
             fullImmersiveTaskId = null
             topTransparentFullscreenTaskId = null
+            pipTaskId = null
+            pipShouldKeepDesktopActive = true
         }
     }
 
@@ -104,6 +115,9 @@
     /* Tracks last bounds of task before toggled to immersive state. */
     private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>()
 
+    /* Callback for when a pending PiP transition has been aborted. */
+    private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null
+
     private var desktopGestureExclusionListener: Consumer<Region>? = null
     private var desktopGestureExclusionExecutor: Executor? = null
 
@@ -302,6 +316,54 @@
         }
     }
 
+    /** Set whether the given task is the Desktop-entered PiP task in this display. */
+    fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) {
+        val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId)
+        if (enterPip) {
+            desktopData.pipTaskId = taskId
+            desktopData.pipShouldKeepDesktopActive = true
+        } else {
+            desktopData.pipTaskId =
+                if (desktopData.pipTaskId == taskId) null
+                else {
+                    logW(
+                        "setTaskInPip: taskId=$taskId did not match saved taskId=${desktopData.pipTaskId}"
+                    )
+                    desktopData.pipTaskId
+                }
+        }
+        notifyVisibleTaskListeners(displayId, getVisibleTaskCount(displayId))
+    }
+
+    /** Returns whether there is a PiP that was entered/minimized from Desktop in this display. */
+    fun isMinimizedPipPresentInDisplay(displayId: Int): Boolean =
+        desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId != null
+
+    /** Returns whether the given task is the Desktop-entered PiP task in this display. */
+    fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean =
+        desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId == taskId
+
+    /** Returns whether Desktop session should be active in this display due to active PiP. */
+    fun shouldDesktopBeActiveForPip(displayId: Int): Boolean =
+        Flags.enableDesktopWindowingPip() &&
+            isMinimizedPipPresentInDisplay(displayId) &&
+            desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive
+
+    /** Saves whether a PiP window should keep Desktop session active in this display. */
+    fun setPipShouldKeepDesktopActive(displayId: Int, keepActive: Boolean) {
+        desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive = keepActive
+    }
+
+    /** Saves callback to handle a pending PiP transition being aborted. */
+    fun setOnPipAbortedCallback(callbackIfPipAborted: ((Int, Int) -> Unit)?) {
+        onPipAbortedCallback = callbackIfPipAborted
+    }
+
+    /** Invokes callback to handle a pending PiP transition with the given task id being aborted. */
+    fun onPipAborted(displayId: Int, pipTaskId: Int) {
+        onPipAbortedCallback?.invoke(displayId, pipTaskId)
+    }
+
     /** Set whether the given task is the full-immersive task in this display. */
     fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) {
         val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId)
@@ -338,8 +400,12 @@
     }
 
     private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
+        val visibleAndPipTasksCount =
+            if (shouldDesktopBeActiveForPip(displayId)) visibleTasksCount + 1 else visibleTasksCount
         visibleTasksListeners.forEach { (listener, executor) ->
-            executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
+            executor.execute {
+                listener.onTasksVisibilityChanged(displayId, visibleAndPipTasksCount)
+            }
         }
     }
 
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 050dfb6..6013648 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
@@ -225,7 +225,6 @@
     // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
     // Used to prevent handleRequest from moving the new fullscreen task to freeform.
     private var dragAndDropFullscreenCookie: Binder? = null
-    private var pendingPipTransitionAndTask: Pair<IBinder, Int>? = null
 
     init {
         desktopMode = DesktopModeImpl()
@@ -321,18 +320,29 @@
     fun visibleTaskCount(displayId: Int): Int = taskRepository.getVisibleTaskCount(displayId)
 
     /**
-     * Returns true if any freeform tasks are visible or if a transparent fullscreen task exists on
-     * top in Desktop Mode.
+     * Returns true if any of the following is true:
+     * - Any freeform tasks are visible
+     * - A transparent fullscreen task exists on top in Desktop Mode
+     * - PiP on Desktop Windowing is enabled, there is an active PiP window and the desktop
+     *   wallpaper is visible.
      */
     fun isDesktopModeShowing(displayId: Int): Boolean {
+        val hasVisibleTasks = visibleTaskCount(displayId) > 0
+        val hasTopTransparentFullscreenTask =
+            taskRepository.getTopTransparentFullscreenTaskId(displayId) != null
+        val hasMinimizedPip =
+            Flags.enableDesktopWindowingPip() &&
+                taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
+                desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(displayId)
         if (
             DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
                 .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
         ) {
-            return visibleTaskCount(displayId) > 0 ||
-                taskRepository.getTopTransparentFullscreenTaskId(displayId) != null
+            return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip
+        } else if (Flags.enableDesktopWindowingPip()) {
+            return hasVisibleTasks || hasMinimizedPip
         }
-        return visibleTaskCount(displayId) > 0
+        return hasVisibleTasks
     }
 
     /** Moves focused task to desktop mode for given [displayId]. */
@@ -592,7 +602,7 @@
     ): ((IBinder) -> Unit)? {
         val taskId = taskInfo.taskId
         desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
-        performDesktopExitCleanupIfNeeded(taskId, displayId, wct)
+        performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
         taskRepository.addClosingTask(displayId, taskId)
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
             doesAnyTaskRequireTaskbarRounding(displayId, taskId)
@@ -624,8 +634,12 @@
                 )
             val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
             wct.merge(requestRes.second, true)
-            pendingPipTransitionAndTask =
-                freeformTaskTransitionStarter.startPipTransition(wct) to taskInfo.taskId
+            freeformTaskTransitionStarter.startPipTransition(wct)
+            taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true)
+            taskRepository.setOnPipAbortedCallback { displayId, taskId ->
+                minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!)
+                taskRepository.setTaskInPip(displayId, taskId, enterPip = false)
+            }
             return
         }
 
@@ -636,7 +650,7 @@
         val taskId = taskInfo.taskId
         val displayId = taskInfo.displayId
         val wct = WindowContainerTransaction()
-        performDesktopExitCleanupIfNeeded(taskId, displayId, wct)
+        performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
         // Notify immersive handler as it might need to exit immersive state.
         val exitResult =
             desktopImmersiveController.exitImmersiveIfApplicable(
@@ -898,7 +912,12 @@
         }
 
         if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
-            performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
+            performDesktopExitCleanupIfNeeded(
+                task.taskId,
+                task.displayId,
+                wct,
+                forceToFullscreen = false,
+            )
         }
 
         transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
@@ -1414,7 +1433,9 @@
         taskId: Int,
         displayId: Int,
         wct: WindowContainerTransaction,
+        forceToFullscreen: Boolean,
     ) {
+        taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen)
         if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
             if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) {
                 return
@@ -1422,6 +1443,12 @@
             if (displayId != DEFAULT_DISPLAY) {
                 return
             }
+        } else if (
+            Flags.enableDesktopWindowingPip() &&
+                taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
+                !forceToFullscreen
+        ) {
+            return
         } else {
             if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
                 return
@@ -1462,21 +1489,6 @@
         return false
     }
 
-    override fun onTransitionConsumed(
-        transition: IBinder,
-        aborted: Boolean,
-        finishT: Transaction?,
-    ) {
-        pendingPipTransitionAndTask?.let { (pipTransition, taskId) ->
-            if (transition == pipTransition) {
-                if (aborted) {
-                    shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { minimizeTaskInner(it) }
-                }
-                pendingPipTransitionAndTask = null
-            }
-        }
-    }
-
     override fun handleRequest(
         transition: IBinder,
         request: TransitionRequestInfo,
@@ -1926,7 +1938,12 @@
         if (!isDesktopModeShowing(task.displayId)) return null
 
         val wct = WindowContainerTransaction()
-        performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
+        performDesktopExitCleanupIfNeeded(
+            task.taskId,
+            task.displayId,
+            wct,
+            forceToFullscreen = false,
+        )
 
         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
             taskRepository.addClosingTask(task.displayId, task.taskId)
@@ -2053,7 +2070,12 @@
             wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
         }
 
-        performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct)
+        performDesktopExitCleanupIfNeeded(
+            taskInfo.taskId,
+            taskInfo.displayId,
+            wct,
+            forceToFullscreen = true,
+        )
     }
 
     private fun cascadeWindow(bounds: Rect, displayLayout: DisplayLayout, displayId: Int) {
@@ -2087,7 +2109,12 @@
         // want it overridden in multi-window.
         wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
 
-        performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct)
+        performDesktopExitCleanupIfNeeded(
+            taskInfo.taskId,
+            taskInfo.displayId,
+            wct,
+            forceToFullscreen = false,
+        )
     }
 
     /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
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 9bf5555..d61ffda 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
@@ -21,9 +21,11 @@
 import android.content.Context
 import android.os.IBinder
 import android.view.SurfaceControl
-import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
 import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.DesktopModeFlags
 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
 import android.window.TransitionInfo
@@ -39,6 +41,8 @@
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
+import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
 
 /**
  * A [Transitions.TransitionObserver] that observes shell transitions and updates the
@@ -57,6 +61,8 @@
 ) : Transitions.TransitionObserver {
 
     private var transitionToCloseWallpaper: IBinder? = null
+    /* Pending PiP transition and its associated display id and task id. */
+    private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null
     private var currentProfileId: Int
 
     init {
@@ -90,6 +96,33 @@
             removeTaskIfNeeded(info)
         }
         removeWallpaperOnLastTaskClosingIfNeeded(transition, info)
+
+        val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
+        info.changes.forEach { change ->
+            change.taskInfo?.let { taskInfo ->
+                if (
+                    Flags.enableDesktopWindowingPip() &&
+                        desktopRepository.isTaskMinimizedPipInDisplay(
+                            taskInfo.displayId,
+                            taskInfo.taskId,
+                        )
+                ) {
+                    when (info.type) {
+                        TRANSIT_PIP ->
+                            pendingPipTransitionAndPipTask =
+                                Triple(transition, taskInfo.displayId, taskInfo.taskId)
+
+                        TRANSIT_EXIT_PIP,
+                        TRANSIT_REMOVE_PIP ->
+                            desktopRepository.setTaskInPip(
+                                taskInfo.displayId,
+                                taskInfo.taskId,
+                                enterPip = false,
+                            )
+                    }
+                }
+            }
+        }
     }
 
     private fun removeTaskIfNeeded(info: TransitionInfo) {
@@ -252,6 +285,18 @@
                 }
             }
             transitionToCloseWallpaper = null
+        } else if (pendingPipTransitionAndPipTask?.first == transition) {
+            val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
+            if (aborted) {
+                pendingPipTransitionAndPipTask?.let {
+                    desktopRepository.onPipAborted(
+                        /*displayId=*/ it.second,
+                        /* taskId=*/ it.third,
+                    )
+                }
+            }
+            desktopRepository.setOnPipAbortedCallback(null)
+            pendingPipTransitionAndPipTask = null
         }
     }
 
@@ -263,11 +308,15 @@
             change.taskInfo?.let { taskInfo ->
                 if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) {
                     when (change.mode) {
-                        WindowManager.TRANSIT_OPEN -> {
+                        TRANSIT_OPEN -> {
                             desktopWallpaperActivityTokenProvider.setToken(
                                 taskInfo.token,
                                 taskInfo.displayId,
                             )
+                            desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+                                isVisible = true,
+                                taskInfo.displayId,
+                            )
                             // After the task for the wallpaper is created, set it non-trimmable.
                             // This is important to prevent recents from trimming and removing the
                             // task.
@@ -278,6 +327,16 @@
                         }
                         TRANSIT_CLOSE ->
                             desktopWallpaperActivityTokenProvider.removeToken(taskInfo.displayId)
+                        TRANSIT_TO_FRONT ->
+                            desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+                                isVisible = true,
+                                taskInfo.displayId,
+                            )
+                        TRANSIT_TO_BACK ->
+                            desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+                                isVisible = false,
+                                taskInfo.displayId,
+                            )
                         else -> {}
                     }
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
index a87004c..2bd7a98 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
@@ -17,6 +17,7 @@
 package com.android.wm.shell.desktopmode.desktopwallpaperactivity
 
 import android.util.SparseArray
+import android.util.SparseBooleanArray
 import android.view.Display.DEFAULT_DISPLAY
 import android.window.WindowContainerToken
 
@@ -24,6 +25,7 @@
 class DesktopWallpaperActivityTokenProvider {
 
     private val wallpaperActivityTokenByDisplayId = SparseArray<WindowContainerToken>()
+    private val wallpaperActivityVisByDisplayId = SparseBooleanArray()
 
     fun setToken(token: WindowContainerToken, displayId: Int = DEFAULT_DISPLAY) {
         wallpaperActivityTokenByDisplayId[displayId] = token
@@ -36,4 +38,16 @@
     fun removeToken(displayId: Int = DEFAULT_DISPLAY) {
         wallpaperActivityTokenByDisplayId.delete(displayId)
     }
+
+    fun setWallpaperActivityIsVisible(
+        isVisible: Boolean = false,
+        displayId: Int = DEFAULT_DISPLAY,
+    ) {
+        wallpaperActivityVisByDisplayId.put(displayId, isVisible)
+    }
+
+    fun isWallpaperActivityVisible(displayId: Int = DEFAULT_DISPLAY): Boolean {
+        return wallpaperActivityTokenByDisplayId[displayId] != null &&
+            wallpaperActivityVisByDisplayId.get(displayId, false)
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
index 7f673d2..ea8dac9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
@@ -40,6 +40,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
@@ -59,6 +60,8 @@
     private final ShellExecutor mMainExecutor;
     private final PipTransitionState mPipTransitionState;
     private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
+    private final Optional<DesktopWallpaperActivityTokenProvider>
+            mDesktopWallpaperActivityTokenProviderOptional;
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     private PipTransitionController mPipTransitionController;
     private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
@@ -73,12 +76,16 @@
             ShellExecutor mainExecutor,
             PipTransitionState pipTransitionState,
             Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+            Optional<DesktopWallpaperActivityTokenProvider>
+                    desktopWallpaperActivityTokenProviderOptional,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
         mContext = context;
         mPipBoundsState = pipBoundsState;
         mMainExecutor = mainExecutor;
         mPipTransitionState = pipTransitionState;
         mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
+        mDesktopWallpaperActivityTokenProviderOptional =
+                desktopWallpaperActivityTokenProviderOptional;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
 
         mSurfaceControlTransactionFactory =
@@ -260,10 +267,18 @@
 
     /** Returns whether PiP is exiting while we're in desktop mode. */
     private boolean isPipExitingToDesktopMode() {
-        return Flags.enableDesktopWindowingPip() && mDesktopUserRepositoriesOptional.isPresent()
-                && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
-                Objects.requireNonNull(mPipTransitionState.getPipTaskInfo()).displayId) > 0
-                || isDisplayInFreeform());
+        // Early return if PiP in Desktop Windowing is not supported.
+        if (!Flags.enableDesktopWindowingPip() || mDesktopUserRepositoriesOptional.isEmpty()
+                || mDesktopWallpaperActivityTokenProviderOptional.isEmpty()) {
+            return false;
+        }
+        final int displayId = Objects.requireNonNull(
+                mPipTransitionState.getPipTaskInfo()).displayId;
+        return mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(displayId)
+                > 0
+                || mDesktopWallpaperActivityTokenProviderOptional.get().isWallpaperActivityVisible(
+                displayId)
+                || isDisplayInFreeform();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 8061ee9..38015ca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -63,7 +63,9 @@
 import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.common.split.SplitScreenUtils;
+import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
 import com.android.wm.shell.pip2.animation.PipEnterAnimator;
@@ -110,6 +112,8 @@
     private final PipTransitionState mPipTransitionState;
     private final PipDisplayLayoutState mPipDisplayLayoutState;
     private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
+    private final Optional<DesktopWallpaperActivityTokenProvider>
+            mDesktopWallpaperActivityTokenProviderOptional;
 
     //
     // Transition caches
@@ -145,7 +149,9 @@
             PipTransitionState pipTransitionState,
             PipDisplayLayoutState pipDisplayLayoutState,
             PipUiStateChangeController pipUiStateChangeController,
-            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) {
+            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+            Optional<DesktopWallpaperActivityTokenProvider>
+                    desktopWallpaperActivityTokenProviderOptional) {
         super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
                 pipBoundsAlgorithm);
 
@@ -157,6 +163,8 @@
         mPipTransitionState.addPipTransitionStateChangedListener(this);
         mPipDisplayLayoutState = pipDisplayLayoutState;
         mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
+        mDesktopWallpaperActivityTokenProviderOptional =
+                desktopWallpaperActivityTokenProviderOptional;
     }
 
     @Override
@@ -826,13 +834,14 @@
             return false;
         }
 
-
         // Since opening a new task while in Desktop Mode always first open in Fullscreen
         // until DesktopMode Shell code resolves it to Freeform, PipTransition will get a
         // possibility to handle it also. In this case return false to not have it enter PiP.
         final boolean isInDesktopSession = !mDesktopUserRepositoriesOptional.isEmpty()
-                && mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
-                pipTask.displayId) > 0;
+                && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
+                        pipTask.displayId) > 0
+                    || mDesktopUserRepositoriesOptional.get().getCurrent()
+                        .isMinimizedPipPresentInDisplay(pipTask.displayId));
         if (isInDesktopSession) {
             return false;
         }
@@ -968,6 +977,27 @@
                         "Unexpected bundle for " + mPipTransitionState);
                 break;
             case PipTransitionState.EXITED_PIP:
+                final TaskInfo pipTask = mPipTransitionState.getPipTaskInfo();
+                final boolean desktopPipEnabled = Flags.enableDesktopWindowingPip()
+                        && mDesktopUserRepositoriesOptional.isPresent()
+                        && mDesktopWallpaperActivityTokenProviderOptional.isPresent();
+                if (desktopPipEnabled && pipTask != null) {
+                    final DesktopRepository desktopRepository =
+                            mDesktopUserRepositoriesOptional.get().getCurrent();
+                    final boolean wallpaperIsVisible =
+                            mDesktopWallpaperActivityTokenProviderOptional.get()
+                                    .isWallpaperActivityVisible(pipTask.displayId);
+                    if (desktopRepository.getVisibleTaskCount(pipTask.displayId) == 0
+                            && wallpaperIsVisible) {
+                        mTransitions.startTransition(
+                                TRANSIT_TO_BACK,
+                                new WindowContainerTransaction().reorder(
+                                        mDesktopWallpaperActivityTokenProviderOptional.get()
+                                                .getToken(pipTask.displayId), /* onTop= */ false),
+                                null
+                        );
+                    }
+                }
                 mPipTransitionState.setPinnedTaskLeash(null);
                 mPipTransitionState.setPipTaskInfo(null);
                 break;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
index 32c79a2..8cdb8c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
@@ -17,9 +17,10 @@
 package com.android.wm.shell.recents;
 
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.view.RemoteAnimationTarget;
 import android.window.TaskSnapshot;
-import android.os.Bundle;
+import android.window.TransitionInfo;
 
 import com.android.wm.shell.recents.IRecentsAnimationController;
 
@@ -57,7 +58,8 @@
      */
     void onAnimationStart(in IRecentsAnimationController controller,
             in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers,
-            in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras) = 2;
+            in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras,
+            in TransitionInfo info) = 2;
 
     /**
      * Called when the task of an activity that has been started while the recents animation
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 76496b0..aeccd86 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -411,10 +411,12 @@
             mInstanceId = System.identityHashCode(this);
             mListener = listener;
             mDeathHandler = () -> {
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
-                finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */,
-                        "deathRecipient");
+                mExecutor.execute(() -> {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                            "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
+                    finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */,
+                            "deathRecipient");
+                });
             };
             try {
                 mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */);
@@ -585,7 +587,8 @@
                 mListener.onAnimationStart(this,
                         apps.toArray(new RemoteAnimationTarget[apps.size()]),
                         new RemoteAnimationTarget[0],
-                        new Rect(0, 0, 0, 0), new Rect(), new Bundle());
+                        new Rect(0, 0, 0, 0), new Rect(), new Bundle(),
+                        null);
                 for (int i = 0; i < mStateListeners.size(); i++) {
                     mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING);
                 }
@@ -816,7 +819,7 @@
                 mListener.onAnimationStart(this,
                         apps.toArray(new RemoteAnimationTarget[apps.size()]),
                         wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]),
-                        new Rect(0, 0, 0, 0), new Rect(), b);
+                        new Rect(0, 0, 0, 0), new Rect(), b, info);
                 for (int i = 0; i < mStateListeners.size(); i++) {
                     mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING);
                 }
@@ -1273,6 +1276,11 @@
                     "requested"));
         }
 
+        /**
+         * @param runnerFinishCb The remote finish callback to run after finish is complete, this is
+         *                       not the same as mFinishCb which reports the transition is finished
+         *                       to WM.
+         */
         private void finishInner(boolean toHome, boolean sendUserLeaveHint,
                 IResultReceiver runnerFinishCb, String reason) {
             if (finishSyntheticTransition(runnerFinishCb, reason)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 5aa3291..b6bd879 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2974,9 +2974,11 @@
             final int transitType = info.getType();
             TransitionInfo.Change pipChange = null;
             int closingSplitTaskId = -1;
-            // This array tracks if we are sending stages TO_BACK in this transition.
-            // TODO (b/349828130): Update for n apps
-            boolean[] stagesSentToBack = new boolean[2];
+            // This array tracks where we are sending stages (TO_BACK/TO_FRONT) in this transition.
+            // TODO (b/349828130): Update for n apps (needs to handle different indices than 0/1).
+            //  Also make sure having multiple changes per stage (2+ tasks in one stage) is being
+            //  handled properly.
+            int[] stageChanges = new int[2];
 
             for (int iC = 0; iC < info.getChanges().size(); ++iC) {
                 final TransitionInfo.Change change = info.getChanges().get(iC);
@@ -3040,18 +3042,25 @@
                                 + " with " + taskId + " before startAnimation().");
                     }
                 }
-                if (isClosingType(change.getMode()) &&
-                        getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED) {
 
-                    // Record which stages are getting sent to back
-                    if (change.getMode() == TRANSIT_TO_BACK) {
-                        stagesSentToBack[getStageOfTask(taskId)] = true;
-                    }
-
+                final int stageOfTaskId = getStageOfTask(taskId);
+                if (stageOfTaskId == STAGE_TYPE_UNDEFINED) {
+                    continue;
+                }
+                if (isClosingType(change.getMode())) {
                     // (For PiP transitions) If either one of the 2 stages is closing we're assuming
                     // we'll break split
                     closingSplitTaskId = taskId;
                 }
+                if (transitType == WindowManager.TRANSIT_WAKE) {
+                    // Record which stages are receiving which changes
+                    if ((change.getMode() == TRANSIT_TO_BACK
+                            || change.getMode() == TRANSIT_TO_FRONT)
+                            && (stageOfTaskId == STAGE_TYPE_MAIN
+                            || stageOfTaskId == STAGE_TYPE_SIDE)) {
+                        stageChanges[stageOfTaskId] = change.getMode();
+                    }
+                }
             }
 
             if (pipChange != null) {
@@ -3076,19 +3085,11 @@
                 return true;
             }
 
-            // If keyguard is active, check to see if we have our TO_BACK transitions in order.
-            // This array should either be all false (no split stages sent to back) or all true
-            // (all stages sent to back). In any other case (which can happen with SHOW_ABOVE_LOCKED
-            // apps) we should break split.
-            if (mKeyguardActive) {
-                boolean isFirstStageSentToBack = stagesSentToBack[0];
-                for (boolean b : stagesSentToBack) {
-                    // Compare each boolean to the first one. If any are different, break split.
-                    if (b != isFirstStageSentToBack) {
-                        dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
-                        break;
-                    }
-                }
+            // If keyguard is active, check to see if we have all our stages showing. If one stage
+            // was moved but not the other (which can happen with SHOW_ABOVE_LOCKED apps), we should
+            // break split.
+            if (mKeyguardActive && stageChanges[STAGE_TYPE_MAIN] != stageChanges[STAGE_TYPE_SIDE]) {
+                dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
             }
 
             final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
index d8884f6..f5aaaad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
@@ -33,6 +33,7 @@
 import com.android.wm.shell.shared.TransactionPool;
 
 import java.util.ArrayList;
+import java.util.function.Consumer;
 
 public class DefaultSurfaceAnimator {
 
@@ -58,42 +59,12 @@
         // Animation length is already expected to be scaled.
         va.overrideDurationScale(1.0f);
         va.setDuration(anim.computeDurationHint());
-        va.addUpdateListener(updateListener);
-        va.addListener(new AnimatorListenerAdapter() {
-            // It is possible for the end/cancel to be called more than once, which may cause
-            // issues if the animating surface has already been released. Track the finished
-            // state here to skip duplicate callbacks. See b/252872225.
-            private boolean mFinished;
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                onFinish();
-            }
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                onFinish();
-            }
-
-            private void onFinish() {
-                if (mFinished) return;
-                mFinished = true;
-                // Apply transformation of end state in case the animation is canceled.
-                if (va.getAnimatedFraction() < 1f) {
-                    va.setCurrentFraction(1f);
-                }
-
-                pool.release(transaction);
-                mainExecutor.execute(() -> {
-                    animations.remove(va);
-                    finishCallback.run();
-                });
-                // The update listener can continue to be called after the animation has ended if
-                // end() is called manually again before the finisher removes the animation.
-                // Remove it manually here to prevent animating a released surface.
-                // See b/252872225.
-                va.removeUpdateListener(updateListener);
-            }
+        setupValueAnimator(va, updateListener, (vanim) -> {
+            pool.release(transaction);
+            mainExecutor.execute(() -> {
+                animations.remove(vanim);
+                finishCallback.run();
+            });
         });
         animations.add(va);
     }
@@ -188,4 +159,50 @@
             }
         }
     }
+
+    /**
+     * Setup some callback logic on a value-animator. This helper ensures that a value animator
+     * finishes at its final fraction (1f) and that relevant callbacks are only called once.
+     */
+    public static ValueAnimator setupValueAnimator(ValueAnimator animator,
+            ValueAnimator.AnimatorUpdateListener updateListener,
+            Consumer<ValueAnimator> afterFinish) {
+        animator.addUpdateListener(updateListener);
+        animator.addListener(new AnimatorListenerAdapter() {
+            // It is possible for the end/cancel to be called more than once, which may cause
+            // issues if the animating surface has already been released. Track the finished
+            // state here to skip duplicate callbacks. See b/252872225.
+            private boolean mFinished;
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                onFinish();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                onFinish();
+            }
+
+            private void onFinish() {
+                if (mFinished) return;
+                mFinished = true;
+                // Apply transformation of end state in case the animation is canceled.
+                if (animator.getAnimatedFraction() < 1f) {
+                    animator.setCurrentFraction(1f);
+                }
+                afterFinish.accept(animator);
+                // The update listener can continue to be called after the animation has ended if
+                // end() is called manually again before the finisher removes the animation.
+                // Remove it manually here to prevent animating a released surface.
+                // See b/252872225.
+                animator.removeUpdateListener(updateListener);
+            }
+        });
+        return animator;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 1689bb5..36c3e97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -55,6 +55,7 @@
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
+import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE;
@@ -69,6 +70,7 @@
 import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
 
 import android.animation.Animator;
+import android.animation.ValueAnimator;
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -104,6 +106,7 @@
 import com.android.internal.protolog.ProtoLog;
 import com.android.window.flags.Flags;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.animation.SizeChangeAnimation;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ShellExecutor;
@@ -422,6 +425,14 @@
                             ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish);
                     continue;
                 }
+
+                if (Flags.portWindowSizeAnimation() && isTask
+                        && TransitionInfo.isIndependent(change, info)
+                        && change.getSnapshot() != null) {
+                    startBoundsChangeAnimation(startTransaction, animations, change, onAnimFinish,
+                            mMainExecutor);
+                    continue;
+                }
             }
 
             // Hide the invisible surface directly without animating it if there is a display
@@ -734,6 +745,21 @@
         }
     }
 
+    private void startBoundsChangeAnimation(@NonNull SurfaceControl.Transaction startT,
+            @NonNull ArrayList<Animator> animations, @NonNull TransitionInfo.Change change,
+            @NonNull Runnable finishCb, @NonNull ShellExecutor mainExecutor) {
+        final SizeChangeAnimation sca =
+                new SizeChangeAnimation(change.getStartAbsBounds(), change.getEndAbsBounds());
+        sca.initialize(change.getLeash(), change.getSnapshot(), startT);
+        final ValueAnimator va = sca.buildAnimator(change.getLeash(), change.getSnapshot(),
+                (animator) -> mainExecutor.execute(() -> {
+                    animations.remove(animator);
+                    finishCb.run();
+                }));
+        va.setDuration(DEFAULT_APP_TRANSITION_DURATION);
+        animations.add(va);
+    }
+
     @Nullable
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 9fbda46..429e056 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -126,6 +126,8 @@
 import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
 import com.android.wm.shell.desktopmode.education.AppToWebEducationController;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.recents.RecentsTransitionStateListener;
 import com.android.wm.shell.shared.FocusTransitionListener;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
@@ -157,8 +159,10 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Supplier;
 
 /**
@@ -247,6 +251,7 @@
     private final DesktopModeEventLogger mDesktopModeEventLogger;
     private final DesktopModeUiEventLogger mDesktopModeUiEventLogger;
     private final WindowDecorTaskResourceLoader mTaskResourceLoader;
+    private final RecentsTransitionHandler mRecentsTransitionHandler;
 
     public DesktopModeWindowDecorViewModel(
             Context context,
@@ -282,7 +287,8 @@
             FocusTransitionObserver focusTransitionObserver,
             DesktopModeEventLogger desktopModeEventLogger,
             DesktopModeUiEventLogger desktopModeUiEventLogger,
-            WindowDecorTaskResourceLoader taskResourceLoader) {
+            WindowDecorTaskResourceLoader taskResourceLoader,
+            RecentsTransitionHandler recentsTransitionHandler) {
         this(
                 context,
                 shellExecutor,
@@ -323,7 +329,8 @@
                 focusTransitionObserver,
                 desktopModeEventLogger,
                 desktopModeUiEventLogger,
-                taskResourceLoader);
+                taskResourceLoader,
+                recentsTransitionHandler);
     }
 
     @VisibleForTesting
@@ -367,7 +374,8 @@
             FocusTransitionObserver focusTransitionObserver,
             DesktopModeEventLogger desktopModeEventLogger,
             DesktopModeUiEventLogger desktopModeUiEventLogger,
-            WindowDecorTaskResourceLoader taskResourceLoader) {
+            WindowDecorTaskResourceLoader taskResourceLoader,
+            RecentsTransitionHandler recentsTransitionHandler) {
         mContext = context;
         mMainExecutor = shellExecutor;
         mMainHandler = mainHandler;
@@ -436,6 +444,7 @@
         mDesktopModeEventLogger = desktopModeEventLogger;
         mDesktopModeUiEventLogger = desktopModeUiEventLogger;
         mTaskResourceLoader = taskResourceLoader;
+        mRecentsTransitionHandler = recentsTransitionHandler;
 
         shellInit.addInitCallback(this::onInit, this);
     }
@@ -450,6 +459,10 @@
                 new DesktopModeOnTaskResizeAnimationListener());
         mDesktopTasksController.setOnTaskRepositionAnimationListener(
                 new DesktopModeOnTaskRepositionAnimationListener());
+        if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
+            mRecentsTransitionHandler.addTransitionStateListener(
+                    new DesktopModeRecentsTransitionStateListener());
+        }
         mDisplayController.addDisplayChangingController(mOnDisplayChangingListener);
         try {
             mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener,
@@ -1859,6 +1872,38 @@
         }
     }
 
+    private class DesktopModeRecentsTransitionStateListener
+            implements RecentsTransitionStateListener {
+        final Set<Integer> mAnimatingTaskIds = new HashSet<>();
+
+        @Override
+        public void onTransitionStateChanged(int state) {
+            switch (state) {
+                case RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED:
+                    for (int n = 0; n < mWindowDecorByTaskId.size(); n++) {
+                        int taskId = mWindowDecorByTaskId.keyAt(n);
+                        mAnimatingTaskIds.add(taskId);
+                        setIsRecentsTransitionRunningForTask(taskId, true);
+                    }
+                    return;
+                case RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING:
+                    // No Recents transition running - clean up window decorations
+                    for (int taskId : mAnimatingTaskIds) {
+                        setIsRecentsTransitionRunningForTask(taskId, false);
+                    }
+                    mAnimatingTaskIds.clear();
+                    return;
+                default:
+            }
+        }
+
+        private void setIsRecentsTransitionRunningForTask(int taskId, boolean isRecentsRunning) {
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+            if (decoration == null) return;
+            decoration.setIsRecentsTransitionRunning(isRecentsRunning);
+        }
+    }
+
     private class DragEventListenerImpl
             implements DragPositioningCallbackUtility.DragEventListener {
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 4ac8954..39a989c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -204,6 +204,7 @@
     private final MultiInstanceHelper mMultiInstanceHelper;
     private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
     private final DesktopUserRepositories mDesktopUserRepositories;
+    private boolean mIsRecentsTransitionRunning = false;
 
     private Runnable mLoadAppInfoRunnable;
     private Runnable mSetAppInfoRunnable;
@@ -498,7 +499,7 @@
                 applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop,
                 mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive,
                 mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
-                displayExclusionRegion);
+                displayExclusionRegion, mIsRecentsTransitionRunning);
 
         final WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -869,7 +870,8 @@
             boolean inFullImmersiveMode,
             @NonNull InsetsState displayInsetsState,
             boolean hasGlobalFocus,
-            @NonNull Region displayExclusionRegion) {
+            @NonNull Region displayExclusionRegion,
+            boolean shouldIgnoreCornerRadius) {
         final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
         final boolean isAppHeader =
                 captionLayoutId == R.layout.desktop_mode_app_header;
@@ -1006,13 +1008,19 @@
         relayoutParams.mWindowDecorConfig = windowDecorConfig;
 
         if (DesktopModeStatus.useRoundedCorners()) {
-            relayoutParams.mCornerRadius = taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
-                    ? loadDimensionPixelSize(context.getResources(),
-                    R.dimen.desktop_windowing_freeform_rounded_corner_radius)
-                    : INVALID_CORNER_RADIUS;
+            relayoutParams.mCornerRadius = shouldIgnoreCornerRadius ? INVALID_CORNER_RADIUS :
+                    getCornerRadius(context, relayoutParams.mLayoutResId);
         }
     }
 
+    private static int getCornerRadius(@NonNull Context context, int layoutResId) {
+        if (layoutResId == R.layout.desktop_mode_app_header) {
+            return loadDimensionPixelSize(context.getResources(),
+                    R.dimen.desktop_windowing_freeform_rounded_corner_radius);
+        }
+        return INVALID_CORNER_RADIUS;
+    }
+
     /**
      * If task has focused window decor, return the caption id of the fullscreen caption size
      * resource. Otherwise, return ID_NULL and caption width be set to task width.
@@ -1740,6 +1748,17 @@
     }
 
     /**
+     * Declares whether a Recents transition is currently active.
+     *
+     * <p> When a Recents transition is active we allow that transition to take ownership of the
+     * corner radius of its task surfaces, so each window decoration should stop updating the corner
+     * radius of its task surface during that time.
+     */
+    void setIsRecentsTransitionRunning(boolean isRecentsTransitionRunning) {
+        mIsRecentsTransitionRunning = isRecentsTransitionRunning;
+    }
+
+    /**
      * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button.
      */
     void onMaximizeButtonHoverExit() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 5d1bedb..fa7183ad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -967,4 +967,4 @@
             return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects), mFlags);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java
new file mode 100644
index 0000000..e91a123
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.wm.shell.appzoomout;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.testing.AndroidTestingRunner;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class AppZoomOutControllerTest extends ShellTestCase {
+
+    @Mock private ShellTaskOrganizer mTaskOrganizer;
+    @Mock private DisplayController mDisplayController;
+    @Mock private AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer;
+    @Mock private ShellExecutor mExecutor;
+    @Mock private ActivityManager.RunningTaskInfo mRunningTaskInfo;
+
+    private AppZoomOutController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        Display display = mContext.getDisplay();
+        DisplayLayout displayLayout = new DisplayLayout(mContext, display);
+        when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(displayLayout);
+
+        ShellInit shellInit = spy(new ShellInit(mExecutor));
+        mController = spy(new AppZoomOutController(mContext, shellInit, mTaskOrganizer,
+                mDisplayController, mDisplayAreaOrganizer, mExecutor));
+    }
+
+    @Test
+    public void isHomeTaskFocused_zoomOutForHome() {
+        mRunningTaskInfo.isFocused = true;
+        when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME);
+        mController.onFocusTaskChanged(mRunningTaskInfo);
+
+        verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(true);
+    }
+
+    @Test
+    public void isHomeTaskNotFocused_zoomOutForApp() {
+        mRunningTaskInfo.isFocused = false;
+        when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME);
+        mController.onFocusTaskChanged(mRunningTaskInfo);
+
+        verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(false);
+    }
+}
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 c0ff2f0..9b24c1c 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
@@ -52,6 +52,7 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.whenever
 
 /** Tests for [DesktopModeEventLogger]. */
@@ -90,20 +91,12 @@
 
         val sessionId = desktopModeEventLogger.currentSessionId.get()
         assertThat(sessionId).isNotEqualTo(NO_SESSION_ID)
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
-                /* event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER),
-                /* enter_reason */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER),
-                /* exit_reason */
-                eq(0),
-                /* sessionId */
-                eq(sessionId),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneUiChangedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER,
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER,
+            0,
+            sessionId,
+        )
         verify {
             EventLogTags.writeWmShellEnterDesktopMode(
                 eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason),
@@ -122,20 +115,13 @@
         val sessionId = desktopModeEventLogger.currentSessionId.get()
         assertThat(sessionId).isNotEqualTo(NO_SESSION_ID)
         assertThat(sessionId).isNotEqualTo(previousSessionId)
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
-                /* event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER),
-                /* enter_reason */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER),
-                /* exit_reason */
-                eq(0),
-                /* sessionId */
-                eq(sessionId),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneUiChangedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER,
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER,
+            /* exit_reason */
+            0,
+            sessionId,
+        )
         verify {
             EventLogTags.writeWmShellEnterDesktopMode(
                 eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason),
@@ -149,7 +135,7 @@
     fun logSessionExit_noOngoingSession_doesNotLog() {
         desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT)
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -159,20 +145,13 @@
 
         desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT)
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
-                /* event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT),
-                /* enter_reason */
-                eq(0),
-                /* exit_reason */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT),
-                /* sessionId */
-                eq(sessionId),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneUiChangedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT,
+            /* enter_reason */
+            0,
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT,
+            sessionId,
+        )
         verify {
             EventLogTags.writeWmShellExitDesktopMode(
                 eq(ExitReason.DRAG_TO_EXIT.reason),
@@ -187,7 +166,7 @@
     fun logTaskAdded_noOngoingSession_doesNotLog() {
         desktopModeEventLogger.logTaskAdded(TASK_UPDATE)
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -197,32 +176,19 @@
 
         desktopModeEventLogger.logTaskAdded(TASK_UPDATE)
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                eq(UNSET_MINIMIZE_REASON),
-                eq(UNSET_UNMINIMIZE_REASON),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            UNSET_MINIMIZE_REASON,
+            UNSET_UNMINIMIZE_REASON,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED),
@@ -245,7 +211,7 @@
     fun logTaskRemoved_noOngoingSession_doesNotLog() {
         desktopModeEventLogger.logTaskRemoved(TASK_UPDATE)
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -255,32 +221,19 @@
 
         desktopModeEventLogger.logTaskRemoved(TASK_UPDATE)
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                eq(UNSET_MINIMIZE_REASON),
-                eq(UNSET_UNMINIMIZE_REASON),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            UNSET_MINIMIZE_REASON,
+            UNSET_UNMINIMIZE_REASON,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED),
@@ -303,7 +256,7 @@
     fun logTaskInfoChanged_noOngoingSession_doesNotLog() {
         desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE)
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -313,35 +266,19 @@
 
         desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE)
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
-                ),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                eq(UNSET_MINIMIZE_REASON),
-                eq(UNSET_UNMINIMIZE_REASON),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            UNSET_MINIMIZE_REASON,
+            UNSET_UNMINIMIZE_REASON,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(
@@ -371,37 +308,19 @@
             createTaskUpdate(minimizeReason = MinimizeReason.TASK_LIMIT)
         )
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
-                ),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                /* minimize_reason */
-                eq(MinimizeReason.TASK_LIMIT.reason),
-                /* unminimize_reason */
-                eq(UNSET_UNMINIMIZE_REASON),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            MinimizeReason.TASK_LIMIT.reason,
+            UNSET_UNMINIMIZE_REASON,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(
@@ -431,37 +350,19 @@
             createTaskUpdate(unminimizeReason = UnminimizeReason.TASKBAR_TAP)
         )
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
-                ),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                /* minimize_reason */
-                eq(UNSET_MINIMIZE_REASON),
-                /* unminimize_reason */
-                eq(UnminimizeReason.TASKBAR_TAP.reason),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            UNSET_MINIMIZE_REASON,
+            UnminimizeReason.TASKBAR_TAP.reason,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(
@@ -491,7 +392,7 @@
             createTaskInfo(),
         )
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -509,39 +410,17 @@
             displayController,
         )
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
-                /* resize_trigger */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER
-                ),
-                /* resizing_stage */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE
-                ),
-                /* input_method */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD
-                ),
-                /* desktop_mode_session_id */
-                eq(sessionId),
-                /* instance_id */
-                eq(TASK_SIZE_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_SIZE_UPDATE.uid),
-                /* task_width */
-                eq(TASK_SIZE_UPDATE.taskWidth),
-                /* task_height */
-                eq(TASK_SIZE_UPDATE.taskHeight),
-                /* display_area */
-                eq(DISPLAY_AREA),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskSizeUpdatedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER,
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE,
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD,
+            sessionId,
+            TASK_SIZE_UPDATE.instanceId,
+            TASK_SIZE_UPDATE.uid,
+            TASK_SIZE_UPDATE.taskWidth,
+            TASK_SIZE_UPDATE.taskHeight,
+            DISPLAY_AREA,
+        )
     }
 
     @Test
@@ -552,7 +431,7 @@
             createTaskInfo(),
         )
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -568,39 +447,17 @@
             displayController = displayController,
         )
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
-                /* resize_trigger */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER
-                ),
-                /* resizing_stage */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE
-                ),
-                /* input_method */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD
-                ),
-                /* desktop_mode_session_id */
-                eq(sessionId),
-                /* instance_id */
-                eq(TASK_SIZE_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_SIZE_UPDATE.uid),
-                /* task_width */
-                eq(TASK_SIZE_UPDATE.taskWidth),
-                /* task_height */
-                eq(TASK_SIZE_UPDATE.taskHeight),
-                /* display_area */
-                eq(DISPLAY_AREA),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskSizeUpdatedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER,
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE,
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD,
+            sessionId,
+            TASK_SIZE_UPDATE.instanceId,
+            TASK_SIZE_UPDATE.uid,
+            TASK_SIZE_UPDATE.taskWidth,
+            TASK_SIZE_UPDATE.taskHeight,
+            DISPLAY_AREA,
+        )
     }
 
     private fun startDesktopModeSession(): Int {
@@ -652,6 +509,171 @@
             .build()
     }
 
+    private fun verifyNoLogging() {
+        verify(
+            {
+                FrameworkStatsLog.write(
+                    eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                )
+            },
+            never(),
+        )
+        verify(
+            {
+                FrameworkStatsLog.write(
+                    eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                )
+            },
+            never(),
+        )
+        verify(
+            {
+                FrameworkStatsLog.write(
+                    eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                )
+            },
+            never(),
+        )
+    }
+
+    private fun verifyOnlyOneUiChangedLogging(
+        event: Int,
+        enterReason: Int,
+        exitReason: Int,
+        sessionId: Int,
+    ) {
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+                eq(event),
+                eq(enterReason),
+                eq(exitReason),
+                eq(sessionId),
+            )
+        })
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+            )
+        })
+    }
+
+    private fun verifyOnlyOneTaskUpdateLogging(
+        taskEvent: Int,
+        instanceId: Int,
+        uid: Int,
+        taskHeight: Int,
+        taskWidth: Int,
+        taskX: Int,
+        taskY: Int,
+        sessionId: Int,
+        minimizeReason: Int,
+        unminimizeReason: Int,
+        visibleTaskCount: Int,
+    ) {
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+                eq(taskEvent),
+                eq(instanceId),
+                eq(uid),
+                eq(taskHeight),
+                eq(taskWidth),
+                eq(taskX),
+                eq(taskY),
+                eq(sessionId),
+                eq(minimizeReason),
+                eq(unminimizeReason),
+                eq(visibleTaskCount),
+            )
+        })
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+            )
+        })
+    }
+
+    private fun verifyOnlyOneTaskSizeUpdatedLogging(
+        resizeTrigger: Int,
+        resizingStage: Int,
+        inputMethod: Int,
+        sessionId: Int,
+        instanceId: Int,
+        uid: Int,
+        taskWidth: Int,
+        taskHeight: Int,
+        displayArea: Int,
+    ) {
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+                eq(resizeTrigger),
+                eq(resizingStage),
+                eq(inputMethod),
+                eq(sessionId),
+                eq(instanceId),
+                eq(uid),
+                eq(taskWidth),
+                eq(taskHeight),
+                eq(displayArea),
+            )
+        })
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+            )
+        })
+    }
+
     private companion object {
         private const val TASK_ID = 1
         private const val TASK_UID = 1
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 5629127..daeccce 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
@@ -25,6 +25,7 @@
 import android.view.Display.INVALID_DISPLAY
 import androidx.test.filters.SmallTest
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.common.ShellExecutor
@@ -1067,6 +1068,67 @@
         assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2)
     }
 
+    @Test
+    fun setTaskInPip_savedAsMinimizedPipInDisplay() {
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse()
+
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+    }
+
+    @Test
+    fun removeTaskInPip_removedAsMinimizedPipInDisplay() {
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false)
+
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse()
+    }
+
+    @Test
+    fun setTaskInPip_multipleDisplays_bothAreInPip() {
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true)
+
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun setPipShouldKeepDesktopActive_shouldKeepDesktopActive() {
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = true)
+
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun setPipShouldNotKeepDesktopActive_shouldNotKeepDesktopActive() {
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+
+        repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = false)
+
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun removeTaskInPip_shouldNotKeepDesktopActive() {
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false)
+
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+    }
+
     class TestListener : DesktopRepository.ActiveTasksListener {
         var activeChangesOnDefaultDisplay = 0
         var activeChangesOnSecondaryDisplay = 0
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 692b503..4bb7430 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
@@ -82,6 +82,7 @@
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.window.flags.Flags
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
 import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS
 import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP
 import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT
@@ -569,6 +570,38 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun isDesktopModeShowing_minimizedPipTask_wallpaperVisible_returnsTrue() {
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+        whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible())
+            .thenReturn(true)
+
+        taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+
+        assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun isDesktopModeShowing_minimizedPipTask_wallpaperNotVisible_returnsFalse() {
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+        whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible())
+            .thenReturn(false)
+
+        taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+
+        assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun isDesktopModeShowing_pipTaskNotMinimizedNorVisible_returnsFalse() {
+        setUpPipTask(autoEnterEnabled = true)
+
+        assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse()
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
         val homeTask = setUpHomeTask(SECOND_DISPLAY)
@@ -2039,6 +2072,41 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun onDesktopWindowClose_minimizedPipPresent_doesNotExitDesktop() {
+        val freeformTask = setUpFreeformTask().apply { isFocused = true }
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+
+        taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask)
+
+        verifyExitDesktopWCTNotExecuted()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun onDesktopWindowClose_minimizedPipNotPresent_exitDesktop() {
+        val freeformTask = setUpFreeformTask()
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+        val handler = mock(TransitionHandler::class.java)
+        whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+            .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+        controller.minimizeTask(pipTask)
+        verifyExitDesktopWCTNotExecuted()
+
+        taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = false)
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask)
+
+        // Remove wallpaper operation
+        wct.hierarchyOps.any { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
     fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
         val task = setUpFreeformTask(active = false)
         val transition = Binder()
@@ -2055,10 +2123,9 @@
     }
 
     @Test
-    fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() {
+    fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() {
         val task = setUpPipTask(autoEnterEnabled = true)
         val handler = mock(TransitionHandler::class.java)
-        whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder())
         whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
             .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
 
@@ -2069,7 +2136,7 @@
     }
 
     @Test
-    fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() {
+    fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() {
         val task = setUpPipTask(autoEnterEnabled = false)
         whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
             .thenReturn(Binder())
@@ -2081,6 +2148,22 @@
     }
 
     @Test
+    fun onPipTaskMinimize_doesntRemoveWallpaper() {
+        val task = setUpPipTask(autoEnterEnabled = true)
+        val handler = mock(TransitionHandler::class.java)
+        whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+            .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+        controller.minimizeTask(task)
+
+        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(freeformTaskTransitionStarter).startPipTransition(captor.capture())
+        captor.value.hierarchyOps.none { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
     fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
         val task = setUpFreeformTask(active = true)
         val transition = Binder()
@@ -3125,6 +3208,31 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() {
+        val freeformTask = setUpFreeformTask()
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+        val handler = mock(TransitionHandler::class.java)
+        whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+            .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+        controller.minimizeTask(pipTask)
+        verifyExitDesktopWCTNotExecuted()
+
+        freeformTask.isFocused = true
+        controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[freeformTask.token.asBinder()])
+        assertThat(taskChange.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+        // Remove wallpaper operation
+        wct.hierarchyOps.any { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
     fun removeDesktop_multipleTasks_removesAll() {
         val task1 = setUpFreeformTask()
@@ -4851,7 +4959,8 @@
     }
 
     private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
-        return setUpFreeformTask().apply {
+        // active = false marks the task as non-visible; PiP window doesn't count as visible tasks
+        return setUpFreeformTask(active = false).apply {
             pictureInPictureParams =
                 PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build()
         }
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 89ab65a..96ed214 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
@@ -22,6 +22,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.os.Binder
 import android.os.IBinder
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
@@ -29,7 +30,9 @@
 import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
 import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.IWindowContainerToken
 import android.window.TransitionInfo
 import android.window.TransitionInfo.Change
@@ -38,6 +41,7 @@
 import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
 import com.android.modules.utils.testing.ExtendedMockitoRule
 import com.android.window.flags.Flags
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
 import com.android.wm.shell.MockToken
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.back.BackAnimationController
@@ -47,6 +51,8 @@
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
+import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
@@ -300,6 +306,115 @@
         verify(taskRepository).clearTopTransparentFullscreenTaskId(topTransparentTask.displayId)
     }
 
+    @Test
+    fun transitOpenWallpaper_wallpaperActivityVisibilitySaved() {
+        val wallpaperTask = createWallpaperTaskInfo()
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createOpenChangeTransition(wallpaperTask),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(desktopWallpaperActivityTokenProvider)
+            .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId)
+    }
+
+    @Test
+    fun transitToFrontWallpaper_wallpaperActivityVisibilitySaved() {
+        val wallpaperTask = createWallpaperTaskInfo()
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createToFrontTransition(wallpaperTask),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(desktopWallpaperActivityTokenProvider)
+            .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId)
+    }
+
+    @Test
+    fun transitToBackWallpaper_wallpaperActivityVisibilitySaved() {
+        val wallpaperTask = createWallpaperTaskInfo()
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createToBackTransition(wallpaperTask),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(desktopWallpaperActivityTokenProvider)
+            .setWallpaperActivityIsVisible(isVisible = false, wallpaperTask.displayId)
+    }
+
+    @Test
+    fun transitCloseWallpaper_wallpaperActivityVisibilitySaved() {
+        val wallpaperTask = createWallpaperTaskInfo()
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createCloseTransition(wallpaperTask),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(desktopWallpaperActivityTokenProvider).removeToken(wallpaperTask.displayId)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun pendingPipTransitionAborted_taskRepositoryOnPipAbortedInvoked() {
+        val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+        val pipTransition = Binder()
+        whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+        transitionObserver.onTransitionReady(
+            transition = pipTransition,
+            info = createOpenChangeTransition(task, TRANSIT_PIP),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+        transitionObserver.onTransitionFinished(transition = pipTransition, aborted = true)
+
+        verify(taskRepository).onPipAborted(task.displayId, task.taskId)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun exitPipTransition_taskRepositoryClearTaskInPip() {
+        val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+        whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createOpenChangeTransition(task, type = TRANSIT_EXIT_PIP),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun removePipTransition_taskRepositoryClearTaskInPip() {
+        val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+        whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createOpenChangeTransition(task, type = TRANSIT_REMOVE_PIP),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
+    }
+
     private fun createBackNavigationTransition(
         task: RunningTaskInfo?,
         type: Int = TRANSIT_TO_BACK,
@@ -331,7 +446,7 @@
         task: RunningTaskInfo?,
         type: Int = TRANSIT_OPEN,
     ): TransitionInfo {
-        return TransitionInfo(TRANSIT_OPEN, /* flags= */ 0).apply {
+        return TransitionInfo(type, /* flags= */ 0).apply {
             addChange(
                 Change(mock(), mock()).apply {
                     mode = TRANSIT_OPEN
@@ -369,6 +484,19 @@
         }
     }
 
+    private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo {
+        return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply {
+            addChange(
+                Change(mock(), mock()).apply {
+                    mode = TRANSIT_TO_FRONT
+                    parent = null
+                    taskInfo = task
+                    flags = flags
+                }
+            )
+        }
+    }
+
     private fun getLatestWct(
         @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
         handlerClass: Class<out Transitions.TransitionHandler>? = null,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
index a8aa257..c42f6c3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
@@ -30,6 +30,7 @@
 import static org.mockito.kotlin.VerificationKt.times;
 import static org.mockito.kotlin.VerificationKt.verify;
 
+import android.app.TaskInfo;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Matrix;
@@ -45,7 +46,9 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
@@ -56,6 +59,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Optional;
@@ -83,7 +87,8 @@
     @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;
     @Mock private SurfaceControl.Transaction mMockTransaction;
     @Mock private PipAlphaAnimator mMockAlphaAnimator;
-    @Mock private Optional<DesktopUserRepositories> mMockOptionalDesktopUserRepositories;
+    @Mock private DesktopUserRepositories mMockDesktopUserRepositories;
+    @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider;
     @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
 
     @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@@ -100,9 +105,13 @@
         when(mMockFactory.getTransaction()).thenReturn(mMockTransaction);
         when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
                 .thenReturn(mMockTransaction);
+        when(mMockDesktopUserRepositories.getCurrent())
+                .thenReturn(Mockito.mock(DesktopRepository.class));
+        when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(Mockito.mock(TaskInfo.class));
 
         mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor,
-                mMockPipTransitionState, mMockOptionalDesktopUserRepositories,
+                mMockPipTransitionState, Optional.of(mMockDesktopUserRepositories),
+                Optional.of(mMockDesktopWallpaperActivityTokenProvider),
                 mRootTaskDisplayAreaOrganizer);
         mPipScheduler.setPipTransitionController(mMockPipTransitionController);
         mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
index 894d238..ab43119 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -169,7 +169,7 @@
         final IResultReceiver finishCallback = mock(IResultReceiver.class);
 
         final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner);
-        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any());
 
         // Finish and verify no transition remains and that the provided finish callback is called
         mRecentsTransitionHandler.findController(transition).finish(true /* toHome */,
@@ -184,7 +184,7 @@
         final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
 
         final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner);
-        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any());
 
         mRecentsTransitionHandler.findController(transition).cancel("test");
         mMainExecutor.flushAll();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index ffe8e71..79e9b9c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -59,11 +59,12 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.window.flags.Flags
 import com.android.wm.shell.R
-import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
 import com.android.wm.shell.desktopmode.DesktopImmersiveController
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
+import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
+import com.android.wm.shell.recents.RecentsTransitionStateListener
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.splitscreen.SplitScreenController
@@ -539,7 +540,8 @@
         onLeftSnapClickListenerCaptor.value.invoke()
 
         verify(mockDesktopTasksController, never())
-            .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
+            .snapToHalfScreen(
+                eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
                 eq(ResizeTrigger.MAXIMIZE_BUTTON),
                 eq(InputMethod.UNKNOWN_INPUT_METHOD),
                 eq(decor),
@@ -616,11 +618,12 @@
         onRightSnapClickListenerCaptor.value.invoke()
 
         verify(mockDesktopTasksController, never())
-            .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
+            .snapToHalfScreen(
+                eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
                 eq(ResizeTrigger.MAXIMIZE_BUTTON),
                 eq(InputMethod.UNKNOWN_INPUT_METHOD),
                 eq(decor),
-        )
+            )
     }
 
     @Test
@@ -1223,6 +1226,49 @@
         verify(task2, never()).onExclusionRegionChanged(newRegion)
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+    fun testRecentsTransitionStateListener_requestedState_setsTransitionRunning() {
+        val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+        val decoration = setUpMockDecorationForTask(task)
+        onTaskOpening(task, SurfaceControl())
+
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+
+        verify(decoration).setIsRecentsTransitionRunning(true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+    fun testRecentsTransitionStateListener_nonRunningState_setsTransitionNotRunning() {
+        val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+        val decoration = setUpMockDecorationForTask(task)
+        onTaskOpening(task, SurfaceControl())
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING)
+
+        verify(decoration).setIsRecentsTransitionRunning(false)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+    fun testRecentsTransitionStateListener_requestedAndAnimating_setsTransitionRunningOnce() {
+        val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+        val decoration = setUpMockDecorationForTask(task)
+        onTaskOpening(task, SurfaceControl())
+
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING)
+
+        verify(decoration, times(1)).setIsRecentsTransitionRunning(true)
+    }
+
     private fun createOpenTaskDecoration(
         @WindowingMode windowingMode: Int,
         taskSurface: SurfaceControl = SurfaceControl(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
index b5e8ceb..8af8285 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -40,6 +40,7 @@
 import android.view.WindowInsets.Type.statusBars
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.window.flags.Flags
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
@@ -65,6 +66,8 @@
 import com.android.wm.shell.desktopmode.education.AppHandleEducationController
 import com.android.wm.shell.desktopmode.education.AppToWebEducationController
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
+import com.android.wm.shell.recents.RecentsTransitionHandler
+import com.android.wm.shell.recents.RecentsTransitionStateListener
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
@@ -151,6 +154,7 @@
     protected val mockFocusTransitionObserver = mock<FocusTransitionObserver>()
     protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>()
     protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>()
+    protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>()
     protected val motionEvent = mock<MotionEvent>()
     val displayLayout = mock<DisplayLayout>()
     protected lateinit var spyContext: TestableContext
@@ -164,6 +168,7 @@
     protected lateinit var mockitoSession: StaticMockitoSession
     protected lateinit var shellInit: ShellInit
     internal lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener
+    protected lateinit var desktopModeRecentsTransitionStateListener: RecentsTransitionStateListener
     protected lateinit var displayChangingListener:
             DisplayChangeController.OnDisplayChangingListener
     internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener
@@ -219,7 +224,8 @@
             mockFocusTransitionObserver,
             desktopModeEventLogger,
             mock<DesktopModeUiEventLogger>(),
-            mock<WindowDecorTaskResourceLoader>()
+            mock<WindowDecorTaskResourceLoader>(),
+            mockRecentsTransitionHandler,
         )
         desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
         whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -256,6 +262,13 @@
         verify(displayInsetsController)
             .addGlobalInsetsChangedListener(insetsChangedCaptor.capture())
         desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue
+        val recentsTransitionStateListenerCaptor = argumentCaptor<RecentsTransitionStateListener>()
+        if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
+            verify(mockRecentsTransitionHandler)
+                .addTransitionStateListener(recentsTransitionStateListenerCaptor.capture())
+            desktopModeRecentsTransitionStateListener =
+                recentsTransitionStateListenerCaptor.firstValue
+        }
         val keyguardChangedCaptor =
             argumentCaptor<DesktopModeKeyguardChangeListener>()
         verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture())
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 6b02aef..9ea5fd6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -169,6 +169,7 @@
     private static final boolean DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED = false;
     private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false;
     private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true;
+    private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false;
 
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
@@ -396,6 +397,31 @@
     }
 
     @Test
+    public void updateRelayoutParams_shouldIgnoreCornerRadius_roundedCornersNotSet() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        fillRoundedCornersResources(/* fillValue= */ 30);
+        RelayoutParams relayoutParams = new RelayoutParams();
+
+        DesktopModeWindowDecoration.updateRelayoutParams(
+                relayoutParams,
+                mTestableContext,
+                taskInfo,
+                mMockSplitScreenController,
+                DEFAULT_APPLY_START_TRANSACTION_ON_DRAW,
+                DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP,
+                DEFAULT_IS_STATUSBAR_VISIBLE,
+                DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
+                DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+                new InsetsState(),
+                DEFAULT_HAS_GLOBAL_FOCUS,
+                mExclusionRegion,
+                /* shouldIgnoreCornerRadius= */ true);
+
+        assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS);
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY)
     public void updateRelayoutParams_appHeader_usesTaskDensity() {
         final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources()
@@ -634,7 +660,8 @@
                 /* inFullImmersiveMode */ true,
                 insetsState,
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         // Takes status bar inset as padding, ignores caption bar inset.
         assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
@@ -659,7 +686,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsInsetSource).isFalse();
     }
@@ -683,7 +711,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         // Header is always shown because it's assumed the status bar is always visible.
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -707,7 +736,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
     }
@@ -730,7 +760,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -753,7 +784,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -777,7 +809,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
 
@@ -793,7 +826,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -817,7 +851,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -1480,7 +1515,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
     }
 
     private DesktopModeWindowDecoration createWindowDecoration(
diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp
index e693fcf..dbb8914 100644
--- a/libs/androidfw/ApkAssets.cpp
+++ b/libs/androidfw/ApkAssets.cpp
@@ -162,13 +162,10 @@
   return assets_provider_->GetDebugName();
 }
 
-UpToDate ApkAssets::IsUpToDate() const {
+bool ApkAssets::IsUpToDate() const {
   // Loaders are invalidated by the app, not the system, so assume they are up to date.
-  if (IsLoader()) {
-    return UpToDate::Always;
-  }
-  const auto idmap_res = loaded_idmap_ ? loaded_idmap_->IsUpToDate() : UpToDate::Always;
-  return combine(idmap_res, [this] { return assets_provider_->IsUpToDate(); });
+  return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate())
+                        && assets_provider_->IsUpToDate());
 }
 
 }  // namespace android
diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp
index 11b12eb..2d3c065 100644
--- a/libs/androidfw/AssetsProvider.cpp
+++ b/libs/androidfw/AssetsProvider.cpp
@@ -24,8 +24,9 @@
 #include <ziparchive/zip_archive.h>
 
 namespace android {
-
-static constexpr std::string_view kEmptyDebugString = "<empty>";
+namespace {
+constexpr const char* kEmptyDebugString = "<empty>";
+} // namespace
 
 std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode,
                                             bool* file_exists) const {
@@ -85,9 +86,11 @@
 }
 
 ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path,
-                                     package_property_t flags, ModDate last_mod_time)
-    : zip_handle_(handle), name_(std::move(path)), flags_(flags), last_mod_time_(last_mod_time) {
-}
+                                     package_property_t flags, time_t last_mod_time)
+    : zip_handle_(handle),
+      name_(std::move(path)),
+      flags_(flags),
+      last_mod_time_(last_mod_time) {}
 
 std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path,
                                                              package_property_t flags,
@@ -101,10 +104,10 @@
     return {};
   }
 
-  ModDate mod_date = kInvalidModDate;
+  struct stat sb{.st_mtime = -1};
   // Skip all up-to-date checks if the file won't ever change.
-  if (isKnownWritablePath(path.c_str()) || !isReadonlyFilesystem(GetFileDescriptor(handle))) {
-    if (mod_date = getFileModDate(GetFileDescriptor(handle)); mod_date == kInvalidModDate) {
+  if (!isReadonlyFilesystem(path.c_str())) {
+    if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) {
       // Stat requires execute permissions on all directories path to the file. If the process does
       // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will
       // always have to return true.
@@ -113,7 +116,7 @@
   }
 
   return std::unique_ptr<ZipAssetsProvider>(
-      new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, mod_date));
+      new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime));
 }
 
 std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd,
@@ -134,10 +137,10 @@
     return {};
   }
 
-  ModDate mod_date = kInvalidModDate;
+  struct stat sb{.st_mtime = -1};
   // Skip all up-to-date checks if the file won't ever change.
   if (!isReadonlyFilesystem(released_fd)) {
-    if (mod_date = getFileModDate(released_fd); mod_date == kInvalidModDate) {
+    if (fstat(released_fd, &sb) < 0) {
       // Stat requires execute permissions on all directories path to the file. If the process does
       // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will
       // always have to return true.
@@ -147,7 +150,7 @@
   }
 
   return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider(
-      handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, mod_date));
+      handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime));
 }
 
 std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path,
@@ -279,16 +282,21 @@
   return name_.GetDebugName();
 }
 
-UpToDate ZipAssetsProvider::IsUpToDate() const {
-  if (last_mod_time_ == kInvalidModDate) {
-    return UpToDate::Always;
+bool ZipAssetsProvider::IsUpToDate() const {
+  if (last_mod_time_ == -1) {
+    return true;
   }
-  return fromBool(last_mod_time_ == getFileModDate(GetFileDescriptor(zip_handle_.get())));
+  struct stat sb{};
+  if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) {
+    // If fstat fails on the zip archive, return true so the zip archive the resource system does
+    // attempt to refresh the ApkAsset.
+    return true;
+  }
+  return last_mod_time_ == sb.st_mtime;
 }
 
-DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time)
-    : dir_(std::move(path)), last_mod_time_(last_mod_time) {
-}
+DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time)
+    : dir_(std::move(path)), last_mod_time_(last_mod_time) {}
 
 std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) {
   struct stat sb;
@@ -309,7 +317,7 @@
 
   const bool isReadonly = isReadonlyFilesystem(path.c_str());
   return std::unique_ptr<DirectoryAssetsProvider>(
-      new DirectoryAssetsProvider(std::move(path), isReadonly ? kInvalidModDate : getModDate(sb)));
+      new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime));
 }
 
 std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path,
@@ -338,11 +346,17 @@
   return dir_;
 }
 
-UpToDate DirectoryAssetsProvider::IsUpToDate() const {
-  if (last_mod_time_ == kInvalidModDate) {
-    return UpToDate::Always;
+bool DirectoryAssetsProvider::IsUpToDate() const {
+  if (last_mod_time_ == -1) {
+    return true;
   }
-  return fromBool(last_mod_time_ == getFileModDate(dir_.c_str()));
+  struct stat sb;
+  if (stat(dir_.c_str(), &sb) < 0) {
+    // If stat fails on the zip archive, return true so the zip archive the resource system does
+    // attempt to refresh the ApkAsset.
+    return true;
+  }
+  return last_mod_time_ == sb.st_mtime;
 }
 
 MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary,
@@ -355,14 +369,8 @@
 
 std::unique_ptr<AssetsProvider> MultiAssetsProvider::Create(
     std::unique_ptr<AssetsProvider>&& primary, std::unique_ptr<AssetsProvider>&& secondary) {
-  if (primary == nullptr && secondary == nullptr) {
-    return EmptyAssetsProvider::Create();
-  }
-  if (!primary) {
-    return secondary;
-  }
-  if (!secondary) {
-    return primary;
+  if (primary == nullptr || secondary == nullptr) {
+    return nullptr;
   }
   return std::unique_ptr<MultiAssetsProvider>(new MultiAssetsProvider(std::move(primary),
                                                                       std::move(secondary)));
@@ -389,8 +397,8 @@
   return debug_name_;
 }
 
-UpToDate MultiAssetsProvider::IsUpToDate() const {
-  return combine(primary_->IsUpToDate(), [this] { return secondary_->IsUpToDate(); });
+bool MultiAssetsProvider::IsUpToDate() const {
+  return primary_->IsUpToDate() && secondary_->IsUpToDate();
 }
 
 EmptyAssetsProvider::EmptyAssetsProvider(std::optional<std::string>&& path) :
@@ -430,12 +438,12 @@
   if (path_.has_value()) {
     return *path_;
   }
-  constexpr static std::string kEmpty{kEmptyDebugString};
+  const static std::string kEmpty = kEmptyDebugString;
   return kEmpty;
 }
 
-UpToDate EmptyAssetsProvider::IsUpToDate() const {
-  return UpToDate::Always;
+bool EmptyAssetsProvider::IsUpToDate() const {
+  return true;
 }
 
 }  // namespace android
diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp
index 262e7df..3ecd82b 100644
--- a/libs/androidfw/Idmap.cpp
+++ b/libs/androidfw/Idmap.cpp
@@ -22,10 +22,9 @@
 #include "android-base/logging.h"
 #include "android-base/stringprintf.h"
 #include "android-base/utf8.h"
-#include "androidfw/AssetManager.h"
+#include "androidfw/misc.h"
 #include "androidfw/ResourceTypes.h"
 #include "androidfw/Util.h"
-#include "androidfw/misc.h"
 #include "utils/ByteOrder.h"
 #include "utils/Trace.h"
 
@@ -269,16 +268,11 @@
       configurations_(configs),
       overlay_entries_(overlay_entries),
       string_pool_(std::move(string_pool)),
+      idmap_fd_(
+          android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)),
       overlay_apk_path_(overlay_apk_path),
       target_apk_path_(target_apk_path),
-      idmap_last_mod_time_(kInvalidModDate) {
-  if (!isReadonlyFilesystem(std::string(overlay_apk_path_).c_str()) ||
-      !(target_apk_path_ == AssetManager::TARGET_APK_PATH ||
-        isReadonlyFilesystem(std::string(target_apk_path_).c_str()))) {
-    idmap_fd_.reset(
-        android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH));
-    idmap_last_mod_time_ = getFileModDate(idmap_fd_);
-  }
+      idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) {
 }
 
 std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) {
@@ -387,11 +381,8 @@
                       overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path));
 }
 
-UpToDate LoadedIdmap::IsUpToDate() const {
-  if (idmap_last_mod_time_ == kInvalidModDate) {
-    return UpToDate::Always;
-  }
-  return fromBool(idmap_last_mod_time_ == getFileModDate(idmap_fd_.get()));
+bool LoadedIdmap::IsUpToDate() const {
+  return idmap_last_mod_time_ == getFileModDate(idmap_fd_.get());
 }
 
 }  // namespace android
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index a8eb062..de9991a 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -152,11 +152,12 @@
     patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t));
 }
 
-void Res_value::copyFrom_dtoh_slow(const Res_value& src) {
-  size = dtohs(src.size);
-  res0 = src.res0;
-  dataType = src.dataType;
-  data = dtohl(src.data);
+void Res_value::copyFrom_dtoh(const Res_value& src)
+{
+    size = dtohs(src.size);
+    res0 = src.res0;
+    dataType = src.dataType;
+    data = dtohl(src.data);
 }
 
 void Res_png_9patch::deviceToFile()
@@ -2030,6 +2031,16 @@
 // --------------------------------------------------------------------
 // --------------------------------------------------------------------
 
+void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) {
+    const size_t size = dtohl(o.size);
+    if (size >= sizeof(ResTable_config)) {
+        *this = o;
+    } else {
+        memcpy(this, &o, size);
+        memset(((uint8_t*)this)+size, 0, sizeof(ResTable_config)-size);
+    }
+}
+
 /* static */ size_t unpackLanguageOrRegion(const char in[2], const char base,
         char out[4]) {
   if (in[0] & 0x80) {
@@ -2094,33 +2105,34 @@
     return unpackLanguageOrRegion(this->country, '0', region);
 }
 
-void ResTable_config::copyFromDtoH_slow(const ResTable_config& o) {
-  copyFromDeviceNoSwap(o);
-  size = sizeof(ResTable_config);
-  mcc = dtohs(mcc);
-  mnc = dtohs(mnc);
-  density = dtohs(density);
-  screenWidth = dtohs(screenWidth);
-  screenHeight = dtohs(screenHeight);
-  sdkVersion = dtohs(sdkVersion);
-  minorVersion = dtohs(minorVersion);
-  smallestScreenWidthDp = dtohs(smallestScreenWidthDp);
-  screenWidthDp = dtohs(screenWidthDp);
-  screenHeightDp = dtohs(screenHeightDp);
+
+void ResTable_config::copyFromDtoH(const ResTable_config& o) {
+    copyFromDeviceNoSwap(o);
+    size = sizeof(ResTable_config);
+    mcc = dtohs(mcc);
+    mnc = dtohs(mnc);
+    density = dtohs(density);
+    screenWidth = dtohs(screenWidth);
+    screenHeight = dtohs(screenHeight);
+    sdkVersion = dtohs(sdkVersion);
+    minorVersion = dtohs(minorVersion);
+    smallestScreenWidthDp = dtohs(smallestScreenWidthDp);
+    screenWidthDp = dtohs(screenWidthDp);
+    screenHeightDp = dtohs(screenHeightDp);
 }
 
-void ResTable_config::swapHtoD_slow() {
-  size = htodl(size);
-  mcc = htods(mcc);
-  mnc = htods(mnc);
-  density = htods(density);
-  screenWidth = htods(screenWidth);
-  screenHeight = htods(screenHeight);
-  sdkVersion = htods(sdkVersion);
-  minorVersion = htods(minorVersion);
-  smallestScreenWidthDp = htods(smallestScreenWidthDp);
-  screenWidthDp = htods(screenWidthDp);
-  screenHeightDp = htods(screenHeightDp);
+void ResTable_config::swapHtoD() {
+    size = htodl(size);
+    mcc = htods(mcc);
+    mnc = htods(mnc);
+    density = htods(density);
+    screenWidth = htods(screenWidth);
+    screenHeight = htods(screenHeight);
+    sdkVersion = htods(sdkVersion);
+    minorVersion = htods(minorVersion);
+    smallestScreenWidthDp = htods(smallestScreenWidthDp);
+    screenWidthDp = htods(screenWidthDp);
+    screenHeightDp = htods(screenHeightDp);
 }
 
 /* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) {
@@ -2133,7 +2145,7 @@
     // systems should happen very infrequently (if at all.)
     // The comparison code relies on memcmp low-level optimizations that make it
     // more efficient than strncmp.
-    static constexpr char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'};
+    const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'};
     const char *lScript = l.localeScriptWasComputed ? emptyScript : l.localeScript;
     const char *rScript = r.localeScriptWasComputed ? emptyScript : r.localeScript;
 
diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp
index 86c459f..be55fe8 100644
--- a/libs/androidfw/Util.cpp
+++ b/libs/androidfw/Util.cpp
@@ -32,18 +32,13 @@
 namespace util {
 
 void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out) {
-  static constexpr bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001;
-  if constexpr (kDeviceEndiannessSame) {
-    *out = Utf16ToUtf8({(const char16_t*)src, strnlen16((const char16_t*)src, len)});
-  } else {
-    char buf[5];
-    while (*src && len != 0) {
-      char16_t c = static_cast<char16_t>(dtohs(*src));
-      utf16_to_utf8(&c, 1, buf, sizeof(buf));
-      out->append(buf, strlen(buf));
-      ++src;
-      --len;
-    }
+  char buf[5];
+  while (*src && len != 0) {
+    char16_t c = static_cast<char16_t>(dtohs(*src));
+    utf16_to_utf8(&c, 1, buf, sizeof(buf));
+    out->append(buf, strlen(buf));
+    ++src;
+    --len;
   }
 }
 
@@ -68,10 +63,8 @@
   }
 
   std::string utf8;
-  utf8.resize_and_overwrite(utf8_length, [&utf16](char* data, size_t size) {
-    utf16_to_utf8(utf16.data(), utf16.length(), data, size + 1);
-    return size;
-  });
+  utf8.resize(utf8_length);
+  utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8_length + 1);
   return utf8;
 }
 
diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp
index a1385f2..f7f62c5 100644
--- a/libs/androidfw/ZipUtils.cpp
+++ b/libs/androidfw/ZipUtils.cpp
@@ -87,19 +87,29 @@
     }
 
     bool ReadAtOffset(uint8_t* buf, size_t len, off64_t offset) const override {
-        if (mInputSize < len || offset > mInputSize - len) {
-            return false;
-        }
-
-        const incfs::map_ptr<uint8_t> pos = mInput.offset(offset);
-        if (!pos.verify(len)) {
+        auto in = AccessAtOffset(buf, len, offset);
+        if (!in) {
           return false;
         }
-
-        memcpy(buf, pos.unsafe_ptr(), len);
+        memcpy(buf, in, len);
         return true;
     }
 
+    const uint8_t* AccessAtOffset(uint8_t*, size_t len, off64_t offset) const override {
+      if (offset > mInputSize - len) {
+        return nullptr;
+      }
+      const incfs::map_ptr<uint8_t> pos = mInput.offset(offset);
+      if (!pos.verify(len)) {
+        return nullptr;
+      }
+      return pos.unsafe_ptr();
+    }
+
+    bool IsZeroCopy() const override {
+      return true;
+    }
+
   private:
     const incfs::map_ptr<uint8_t> mInput;
     const size_t mInputSize;
@@ -107,7 +117,7 @@
 
 class BufferWriter final : public zip_archive::Writer {
   public:
-    BufferWriter(void* output, size_t outputSize) : Writer(),
+    BufferWriter(void* output, size_t outputSize) :
         mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) {
     }
 
@@ -121,6 +131,12 @@
         return true;
     }
 
+    Buffer GetBuffer(size_t length) override {
+        const auto remaining_size = mOutputSize - mBytesWritten;
+        return remaining_size >= length
+                   ? Buffer(mOutput + mBytesWritten, remaining_size) : Buffer();
+    }
+
   private:
     uint8_t* const mOutput;
     const size_t mOutputSize;
diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h
index 3f6f466..231808b 100644
--- a/libs/androidfw/include/androidfw/ApkAssets.h
+++ b/libs/androidfw/include/androidfw/ApkAssets.h
@@ -116,7 +116,7 @@
     return resources_asset_ != nullptr && resources_asset_->isAllocated();
   }
 
-  UpToDate IsUpToDate() const;
+  bool IsUpToDate() const;
 
   // DANGER!
   // This is a destructive method that rips the assets provider out of ApkAssets object.
diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h
index e3b3ae4..d33c325 100644
--- a/libs/androidfw/include/androidfw/AssetsProvider.h
+++ b/libs/androidfw/include/androidfw/AssetsProvider.h
@@ -14,7 +14,8 @@
  * limitations under the License.
  */
 
-#pragma once
+#ifndef ANDROIDFW_ASSETSPROVIDER_H
+#define ANDROIDFW_ASSETSPROVIDER_H
 
 #include <memory>
 #include <string>
@@ -57,7 +58,7 @@
   WARN_UNUSED virtual const std::string& GetDebugName() const = 0;
 
   // Returns whether the interface provides the most recent version of its files.
-  WARN_UNUSED virtual UpToDate IsUpToDate() const = 0;
+  WARN_UNUSED virtual bool IsUpToDate() const = 0;
 
   // Creates an Asset from a file on disk.
   static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path);
@@ -94,7 +95,7 @@
 
   WARN_UNUSED std::optional<std::string_view> GetPath() const override;
   WARN_UNUSED const std::string& GetDebugName() const override;
-  WARN_UNUSED UpToDate IsUpToDate() const override;
+  WARN_UNUSED bool IsUpToDate() const override;
   WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const;
 
   ~ZipAssetsProvider() override = default;
@@ -105,7 +106,7 @@
  private:
   struct PathOrDebugName;
   ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, package_property_t flags,
-                    ModDate last_mod_time);
+                    time_t last_mod_time);
 
   struct PathOrDebugName {
     static PathOrDebugName Path(std::string value) {
@@ -134,7 +135,7 @@
   std::unique_ptr<ZipArchive, ZipCloser> zip_handle_;
   PathOrDebugName name_;
   package_property_t flags_;
-  ModDate last_mod_time_;
+  time_t last_mod_time_;
 };
 
 // Supplies assets from a root directory.
@@ -146,7 +147,7 @@
 
   WARN_UNUSED std::optional<std::string_view> GetPath() const override;
   WARN_UNUSED const std::string& GetDebugName() const override;
-  WARN_UNUSED UpToDate IsUpToDate() const override;
+  WARN_UNUSED bool IsUpToDate() const override;
 
   ~DirectoryAssetsProvider() override = default;
  protected:
@@ -155,23 +156,23 @@
                                       bool* file_exists) const override;
 
  private:
-  explicit DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time);
+  explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time);
   std::string dir_;
-  ModDate last_mod_time_;
+  time_t last_mod_time_;
 };
 
 // Supplies assets from a `primary` asset provider and falls back to supplying assets from the
 // `secondary` asset provider if the asset cannot be found in the `primary`.
 struct MultiAssetsProvider : public AssetsProvider {
   static std::unique_ptr<AssetsProvider> Create(std::unique_ptr<AssetsProvider>&& primary,
-                                                std::unique_ptr<AssetsProvider>&& secondary = {});
+                                                std::unique_ptr<AssetsProvider>&& secondary);
 
   bool ForEachFile(const std::string& root_path,
                    base::function_ref<void(StringPiece, FileType)> f) const override;
 
   WARN_UNUSED std::optional<std::string_view> GetPath() const override;
   WARN_UNUSED const std::string& GetDebugName() const override;
-  WARN_UNUSED UpToDate IsUpToDate() const override;
+  WARN_UNUSED bool IsUpToDate() const override;
 
   ~MultiAssetsProvider() override = default;
  protected:
@@ -198,7 +199,7 @@
 
   WARN_UNUSED std::optional<std::string_view> GetPath() const override;
   WARN_UNUSED const std::string& GetDebugName() const override;
-  WARN_UNUSED UpToDate IsUpToDate() const override;
+  WARN_UNUSED bool IsUpToDate() const override;
 
   ~EmptyAssetsProvider() override = default;
  protected:
@@ -211,3 +212,5 @@
 };
 
 }  // namespace android
+
+#endif /* ANDROIDFW_ASSETSPROVIDER_H */
diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h
index 87f3c9d..ac75eb3 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -14,7 +14,8 @@
  * limitations under the License.
  */
 
-#pragma once
+#ifndef IDMAP_H_
+#define IDMAP_H_
 
 #include <memory>
 #include <string>
@@ -31,31 +32,6 @@
 
 namespace android {
 
-// An enum that tracks more states than just 'up to date' or 'not' for a resources container:
-// there are several cases where we know for sure that the object can't change and won't get
-// out of date. Reporting those states to the managed layer allows it to stop checking here
-// completely, speeding up the cache lookups by dozens of milliseconds.
-enum class UpToDate : int { False, True, Always };
-
-// Combines two UpToDate values, and only accesses the second one if it matters to the result.
-template <class Getter>
-UpToDate combine(UpToDate first, Getter secondGetter) {
-  switch (first) {
-    case UpToDate::False:
-      return UpToDate::False;
-    case UpToDate::True: {
-      const auto second = secondGetter();
-      return second == UpToDate::False ? UpToDate::False : UpToDate::True;
-    }
-    case UpToDate::Always:
-      return secondGetter();
-  }
-}
-
-inline UpToDate fromBool(bool value) {
-  return value ? UpToDate::True : UpToDate::False;
-}
-
 class LoadedIdmap;
 class IdmapResMap;
 struct Idmap_header;
@@ -220,7 +196,7 @@
 
   // Returns whether the idmap file on disk has not been modified since the construction of this
   // LoadedIdmap.
-  UpToDate IsUpToDate() const;
+  bool IsUpToDate() const;
 
  protected:
   // Exposed as protected so that tests can subclass and mock this class out.
@@ -255,3 +231,5 @@
 };
 
 }  // namespace android
+
+#endif  // IDMAP_H_
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 819fe4b..e330410 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -47,8 +47,6 @@
 
 namespace android {
 
-constexpr const bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001;
-
 constexpr const uint32_t kIdmapMagic = 0x504D4449u;
 constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Au;
 
@@ -410,16 +408,7 @@
     typedef uint32_t data_type;
     data_type data;
 
-    void copyFrom_dtoh(const Res_value& src) {
-      if constexpr (kDeviceEndiannessSame) {
-        *this = src;
-      } else {
-        copyFrom_dtoh_slow(src);
-      }
-    }
-
-   private:
-    void copyFrom_dtoh_slow(const Res_value& src);
+    void copyFrom_dtoh(const Res_value& src);
 };
 
 /**
@@ -1265,32 +1254,11 @@
     // Varies in length from 3 to 8 chars. Zero-filled value.
     char localeNumberingSystem[8];
 
-    void copyFromDeviceNoSwap(const ResTable_config& o) {
-      const auto o_size = dtohl(o.size);
-      if (o_size >= sizeof(ResTable_config)) [[likely]] {
-        *this = o;
-      } else {
-        memcpy(this, &o, o_size);
-        memset(((uint8_t*)this) + o_size, 0, sizeof(ResTable_config) - o_size);
-      }
-      this->size = sizeof(*this);
-    }
-
-    void copyFromDtoH(const ResTable_config& o) {
-      if constexpr (kDeviceEndiannessSame) {
-        copyFromDeviceNoSwap(o);
-      } else {
-        copyFromDtoH_slow(o);
-      }
-    }
-
-    void swapHtoD() {
-      if constexpr (kDeviceEndiannessSame) {
-        ;  // noop
-      } else {
-        swapHtoD_slow();
-      }
-    }
+    void copyFromDeviceNoSwap(const ResTable_config& o);
+    
+    void copyFromDtoH(const ResTable_config& o);
+    
+    void swapHtoD();
 
     int compare(const ResTable_config& o) const;
     int compareLogical(const ResTable_config& o) const;
@@ -1416,10 +1384,6 @@
     bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const;
 
     String8 toString() const;
-
-   private:
-    void copyFromDtoH_slow(const ResTable_config& o);
-    void swapHtoD_slow();
 };
 
 /**
diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h
index d8ca64a..c9ba8a0 100644
--- a/libs/androidfw/include/androidfw/misc.h
+++ b/libs/androidfw/include/androidfw/misc.h
@@ -15,7 +15,6 @@
  */
 #pragma once
 
-#include <sys/stat.h>
 #include <time.h>
 
 //
@@ -65,15 +64,10 @@
 /* same, but also returns -1 if the file has already been deleted */
 ModDate getFileModDate(int fd);
 
-// Extract the modification date from the stat structure.
-ModDate getModDate(const struct ::stat& st);
-
 // Check if |path| or |fd| resides on a readonly filesystem.
 bool isReadonlyFilesystem(const char* path);
 bool isReadonlyFilesystem(int fd);
 
-bool isKnownWritablePath(const char* path);
-
 }  // namespace android
 
 // Whoever uses getFileModDate() will need this as well
diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp
index 26eb320..32f3624 100644
--- a/libs/androidfw/misc.cpp
+++ b/libs/androidfw/misc.cpp
@@ -16,10 +16,10 @@
 
 #define LOG_TAG "misc"
 
-#include "androidfw/misc.h"
-
-#include <errno.h>
-#include <sys/stat.h>
+//
+// Miscellaneous utility functions.
+//
+#include <androidfw/misc.h>
 
 #include "android-base/logging.h"
 
@@ -28,7 +28,9 @@
 #include <sys/vfs.h>
 #endif  // __linux__
 
-#include <array>
+#include <errno.h>
+#include <sys/stat.h>
+
 #include <cstdio>
 #include <cstring>
 #include <tuple>
@@ -38,26 +40,28 @@
 /*
  * Get a file's type.
  */
-FileType getFileType(const char* fileName) {
-  struct stat sb;
-  if (stat(fileName, &sb) < 0) {
-    if (errno == ENOENT || errno == ENOTDIR)
-      return kFileTypeNonexistent;
-    else {
-      PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed";
-      return kFileTypeUnknown;
-    }
-  } else {
-    if (S_ISREG(sb.st_mode))
-      return kFileTypeRegular;
-    else if (S_ISDIR(sb.st_mode))
-      return kFileTypeDirectory;
-    else if (S_ISCHR(sb.st_mode))
-      return kFileTypeCharDev;
-    else if (S_ISBLK(sb.st_mode))
-      return kFileTypeBlockDev;
-    else if (S_ISFIFO(sb.st_mode))
-      return kFileTypeFifo;
+FileType getFileType(const char* fileName)
+{
+    struct stat sb;
+
+    if (stat(fileName, &sb) < 0) {
+        if (errno == ENOENT || errno == ENOTDIR)
+            return kFileTypeNonexistent;
+        else {
+            PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed";
+            return kFileTypeUnknown;
+        }
+    } else {
+        if (S_ISREG(sb.st_mode))
+            return kFileTypeRegular;
+        else if (S_ISDIR(sb.st_mode))
+            return kFileTypeDirectory;
+        else if (S_ISCHR(sb.st_mode))
+            return kFileTypeCharDev;
+        else if (S_ISBLK(sb.st_mode))
+            return kFileTypeBlockDev;
+        else if (S_ISFIFO(sb.st_mode))
+            return kFileTypeFifo;
 #if defined(S_ISLNK)
         else if (S_ISLNK(sb.st_mode))
             return kFileTypeSymlink;
@@ -71,7 +75,7 @@
     }
 }
 
-ModDate getModDate(const struct stat& st) {
+static ModDate getModDate(const struct stat& st) {
 #ifdef _WIN32
   return st.st_mtime;
 #elif defined(__APPLE__)
@@ -109,14 +113,8 @@
 bool isReadonlyFilesystem(int) {
     return false;
 }
-bool isKnownWritablePath(const char*) {
-  return false;
-}
 #else   // __linux__
 bool isReadonlyFilesystem(const char* path) {
-  if (isKnownWritablePath(path)) {
-    return false;
-  }
     struct statfs sfs;
     if (::statfs(path, &sfs)) {
         PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed";
@@ -133,13 +131,6 @@
     }
     return (sfs.f_flags & ST_RDONLY) != 0;
 }
-
-bool isKnownWritablePath(const char* path) {
-  // We know that all paths in /data/ are writable.
-  static constexpr char kRwPrefix[] = "/data/";
-  return strncmp(kRwPrefix, path, std::size(kRwPrefix) - 1) == 0;
-}
-
 #endif  // __linux__
 
 }  // namespace android
diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp
index 22b9e69..cb2e56f 100644
--- a/libs/androidfw/tests/Idmap_test.cpp
+++ b/libs/androidfw/tests/Idmap_test.cpp
@@ -218,11 +218,10 @@
 
   auto apk_assets = ApkAssets::LoadOverlay(temp_file.path);
   ASSERT_NE(nullptr, apk_assets);
-  ASSERT_TRUE(apk_assets->IsOverlay());
-  ASSERT_EQ(UpToDate::True, apk_assets->IsUpToDate());
+  ASSERT_TRUE(apk_assets->IsUpToDate());
 
   unlink(temp_file.path);
-  ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate());
+  ASSERT_FALSE(apk_assets->IsUpToDate());
 
   const auto sleep_duration =
       std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull));
@@ -231,27 +230,7 @@
   base::WriteStringToFile("hello", temp_file.path);
   std::this_thread::sleep_for(sleep_duration);
 
-  ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate());
-}
-
-TEST(IdmapTestUpToDate, Combine) {
-  ASSERT_EQ(UpToDate::False, combine(UpToDate::False, [] {
-              ADD_FAILURE();  // Shouldn't get called at all.
-              return UpToDate::False;
-            }));
-
-  ASSERT_EQ(UpToDate::False, combine(UpToDate::True, [] { return UpToDate::False; }));
-
-  ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::True; }));
-  ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::Always; }));
-  ASSERT_EQ(UpToDate::True, combine(UpToDate::Always, [] { return UpToDate::True; }));
-
-  ASSERT_EQ(UpToDate::Always, combine(UpToDate::Always, [] { return UpToDate::Always; }));
-}
-
-TEST(IdmapTestUpToDate, FromBool) {
-  ASSERT_EQ(UpToDate::False, fromBool(false));
-  ASSERT_EQ(UpToDate::True, fromBool(true));
+  ASSERT_FALSE(apk_assets->IsUpToDate());
 }
 
 }  // namespace
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 7b45070..290df99 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -36,7 +36,7 @@
     const Typeface* resolvedFace = Typeface::resolveDefault(typeface);
     const SkFont& font = paint->getSkFont();
 
-    minikin::MinikinPaint minikinPaint(resolvedFace->fFontCollection);
+    minikin::MinikinPaint minikinPaint(resolvedFace->getFontCollection());
     /* Prepare minikin Paint */
     minikinPaint.size =
             font.isLinearMetrics() ? font.getSize() : static_cast<int>(font.getSize());
@@ -46,9 +46,9 @@
     minikinPaint.wordSpacing = paint->getWordSpacing();
     minikinPaint.fontFlags = MinikinFontSkia::packFontFlags(font);
     minikinPaint.localeListId = paint->getMinikinLocaleListId();
-    minikinPaint.fontStyle = resolvedFace->fStyle;
+    minikinPaint.fontStyle = resolvedFace->getFontStyle();
     minikinPaint.fontFeatureSettings = paint->getFontFeatureSettings();
-    if (!resolvedFace->fIsVariationInstance) {
+    if (!resolvedFace->isVariationInstance()) {
         // This is an optimization for direct private API use typically done by System UI.
         // In the public API surface, if Typeface is already configured for variation instance
         // (Target SDK <= 35) the font variation settings of Paint is not set.
@@ -132,7 +132,7 @@
 
 bool MinikinUtils::hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs) {
     const Typeface* resolvedFace = Typeface::resolveDefault(typeface);
-    return resolvedFace->fFontCollection->hasVariationSelector(codepoint, vs);
+    return resolvedFace->getFontCollection()->hasVariationSelector(codepoint, vs);
 }
 
 float MinikinUtils::xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout) {
diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp
index 4dfe053..a73aac6 100644
--- a/libs/hwui/hwui/Typeface.cpp
+++ b/libs/hwui/hwui/Typeface.cpp
@@ -70,74 +70,45 @@
 
 Typeface* Typeface::createRelative(Typeface* src, Typeface::Style style) {
     const Typeface* resolvedFace = Typeface::resolveDefault(src);
-    Typeface* result = new Typeface;
-    if (result != nullptr) {
-        result->fFontCollection = resolvedFace->fFontCollection;
-        result->fBaseWeight = resolvedFace->fBaseWeight;
-        result->fAPIStyle = style;
-        result->fStyle = computeRelativeStyle(result->fBaseWeight, style);
-        result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
-    }
-    return result;
+    return new Typeface(resolvedFace->getFontCollection(),
+                        computeRelativeStyle(resolvedFace->getBaseWeight(), style), style,
+                        resolvedFace->getBaseWeight(), resolvedFace->isVariationInstance());
 }
 
 Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) {
     const Typeface* resolvedFace = Typeface::resolveDefault(base);
-    Typeface* result = new Typeface();
-    if (result != nullptr) {
-        result->fFontCollection = resolvedFace->fFontCollection;
-        result->fBaseWeight = resolvedFace->fBaseWeight;
-        result->fAPIStyle = computeAPIStyle(weight, italic);
-        result->fStyle = computeMinikinStyle(weight, italic);
-        result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
-    }
-    return result;
+    return new Typeface(resolvedFace->getFontCollection(), computeMinikinStyle(weight, italic),
+                        computeAPIStyle(weight, italic), resolvedFace->getBaseWeight(),
+                        resolvedFace->isVariationInstance());
 }
 
 Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src,
                                                     const minikin::VariationSettings& variations) {
     const Typeface* resolvedFace = Typeface::resolveDefault(src);
-    Typeface* result = new Typeface();
-    if (result != nullptr) {
-        result->fFontCollection =
-                resolvedFace->fFontCollection->createCollectionWithVariation(variations);
-        if (result->fFontCollection == nullptr) {
+    const std::shared_ptr<minikin::FontCollection>& fc =
+            resolvedFace->getFontCollection()->createCollectionWithVariation(variations);
+    return new Typeface(
             // None of passed axes are supported by this collection.
             // So we will reuse the same collection with incrementing reference count.
-            result->fFontCollection = resolvedFace->fFontCollection;
-        }
-        // Do not update styles.
-        // TODO: We may want to update base weight if the 'wght' is specified.
-        result->fBaseWeight = resolvedFace->fBaseWeight;
-        result->fAPIStyle = resolvedFace->fAPIStyle;
-        result->fStyle = resolvedFace->fStyle;
-        result->fIsVariationInstance = true;
-    }
-    return result;
+            fc ? fc : resolvedFace->getFontCollection(),
+            // Do not update styles.
+            // TODO: We may want to update base weight if the 'wght' is specified.
+            resolvedFace->fStyle, resolvedFace->getAPIStyle(), resolvedFace->getBaseWeight(), true);
 }
 
 Typeface* Typeface::createWithDifferentBaseWeight(Typeface* src, int weight) {
     const Typeface* resolvedFace = Typeface::resolveDefault(src);
-    Typeface* result = new Typeface;
-    if (result != nullptr) {
-        result->fFontCollection = resolvedFace->fFontCollection;
-        result->fBaseWeight = weight;
-        result->fAPIStyle = resolvedFace->fAPIStyle;
-        result->fStyle = computeRelativeStyle(weight, result->fAPIStyle);
-        result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
-    }
-    return result;
+    return new Typeface(resolvedFace->getFontCollection(),
+                        computeRelativeStyle(weight, resolvedFace->getAPIStyle()),
+                        resolvedFace->getAPIStyle(), weight, resolvedFace->isVariationInstance());
 }
 
 Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families,
                                        int weight, int italic, const Typeface* fallback) {
-    Typeface* result = new Typeface;
-    if (fallback == nullptr) {
-        result->fFontCollection = minikin::FontCollection::create(std::move(families));
-    } else {
-        result->fFontCollection =
-                fallback->fFontCollection->createCollectionWithFamilies(std::move(families));
-    }
+    const std::shared_ptr<minikin::FontCollection>& fc =
+            fallback ? fallback->getFontCollection()->createCollectionWithFamilies(
+                               std::move(families))
+                     : minikin::FontCollection::create(std::move(families));
 
     if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) {
         int weightFromFont;
@@ -171,11 +142,8 @@
         weight = SkFontStyle::kNormal_Weight;
     }
 
-    result->fBaseWeight = weight;
-    result->fAPIStyle = computeAPIStyle(weight, italic);
-    result->fStyle = computeMinikinStyle(weight, italic);
-    result->fIsVariationInstance = false;
-    return result;
+    return new Typeface(fc, computeMinikinStyle(weight, italic), computeAPIStyle(weight, italic),
+                        weight, false);
 }
 
 void Typeface::setDefault(const Typeface* face) {
@@ -205,11 +173,8 @@
     std::shared_ptr<minikin::FontCollection> collection =
             minikin::FontCollection::create(minikin::FontFamily::create(std::move(fonts)));
 
-    Typeface* hwTypeface = new Typeface();
-    hwTypeface->fFontCollection = collection;
-    hwTypeface->fAPIStyle = Typeface::kNormal;
-    hwTypeface->fBaseWeight = SkFontStyle::kNormal_Weight;
-    hwTypeface->fStyle = minikin::FontStyle();
+    Typeface* hwTypeface = new Typeface(collection, minikin::FontStyle(), Typeface::kNormal,
+                                        SkFontStyle::kNormal_Weight, false);
 
     Typeface::setDefault(hwTypeface);
 #endif
diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h
index 97d1bf4..e8233a6 100644
--- a/libs/hwui/hwui/Typeface.h
+++ b/libs/hwui/hwui/Typeface.h
@@ -32,21 +32,39 @@
 
 struct ANDROID_API Typeface {
 public:
-    std::shared_ptr<minikin::FontCollection> fFontCollection;
+    enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 };
+    Typeface(const std::shared_ptr<minikin::FontCollection> fc, minikin::FontStyle style,
+             Style apiStyle, int baseWeight, bool isVariationInstance)
+            : fFontCollection(fc)
+            , fStyle(style)
+            , fAPIStyle(apiStyle)
+            , fBaseWeight(baseWeight)
+            , fIsVariationInstance(isVariationInstance) {}
+
+    const std::shared_ptr<minikin::FontCollection>& getFontCollection() const {
+        return fFontCollection;
+    }
 
     // resolved style actually used for rendering
-    minikin::FontStyle fStyle;
+    minikin::FontStyle getFontStyle() const { return fStyle; }
 
     // style used in the API
-    enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 };
-    Style fAPIStyle;
+    Style getAPIStyle() const { return fAPIStyle; }
 
     // base weight in CSS-style units, 1..1000
-    int fBaseWeight;
+    int getBaseWeight() const { return fBaseWeight; }
 
     // True if the Typeface is already created for variation settings.
-    bool fIsVariationInstance;
+    bool isVariationInstance() const { return fIsVariationInstance; }
 
+private:
+    std::shared_ptr<minikin::FontCollection> fFontCollection;
+    minikin::FontStyle fStyle;
+    Style fAPIStyle;
+    int fBaseWeight;
+    bool fIsVariationInstance = false;
+
+public:
     static const Typeface* resolveDefault(const Typeface* src);
 
     // The following three functions create new Typeface from an existing Typeface with a different
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 8d3a5eb..f6fdec1 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -609,7 +609,8 @@
         SkFont* font = &paint->getSkFont();
         const Typeface* typeface = paint->getAndroidTypeface();
         typeface = Typeface::resolveDefault(typeface);
-        minikin::FakedFont baseFont = typeface->fFontCollection->baseFontFaked(typeface->fStyle);
+        minikin::FakedFont baseFont =
+                typeface->getFontCollection()->baseFontFaked(typeface->getFontStyle());
         float saveSkewX = font->getSkewX();
         bool savefakeBold = font->isEmbolden();
         MinikinFontSkia::populateSkFont(font, baseFont.typeface().get(), baseFont.fakery);
@@ -641,7 +642,7 @@
         if (useLocale) {
             minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface);
             minikin::MinikinExtent extent =
-                    typeface->fFontCollection->getReferenceExtentForLocale(minikinPaint);
+                    typeface->getFontCollection()->getReferenceExtentForLocale(minikinPaint);
             metrics->fAscent = std::min(extent.ascent, metrics->fAscent);
             metrics->fDescent = std::max(extent.descent, metrics->fDescent);
             metrics->fTop = std::min(metrics->fAscent, metrics->fTop);
diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp
index c5095c1..63906de 100644
--- a/libs/hwui/jni/Typeface.cpp
+++ b/libs/hwui/jni/Typeface.cpp
@@ -99,17 +99,17 @@
 
 // CriticalNative
 static jint Typeface_getStyle(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
-    return toTypeface(faceHandle)->fAPIStyle;
+    return toTypeface(faceHandle)->getAPIStyle();
 }
 
 // CriticalNative
 static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
-    return toTypeface(faceHandle)->fStyle.weight();
+    return toTypeface(faceHandle)->getFontStyle().weight();
 }
 
 // Critical Native
 static jboolean Typeface_isVariationInstance(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
-    return toTypeface(faceHandle)->fIsVariationInstance;
+    return toTypeface(faceHandle)->isVariationInstance();
 }
 
 static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray,
@@ -128,18 +128,18 @@
 // CriticalNative
 static void Typeface_setDefault(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
     Typeface::setDefault(toTypeface(faceHandle));
-    minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->fFontCollection);
+    minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->getFontCollection());
 }
 
 static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) {
     Typeface* face = toTypeface(faceHandle);
-    const size_t length = face->fFontCollection->getSupportedAxesCount();
+    const size_t length = face->getFontCollection()->getSupportedAxesCount();
     if (length == 0) {
         return nullptr;
     }
     std::vector<jint> tagVec(length);
     for (size_t i = 0; i < length; i++) {
-        tagVec[i] = face->fFontCollection->getSupportedAxisAt(i);
+        tagVec[i] = face->getFontCollection()->getSupportedAxisAt(i);
     }
     std::sort(tagVec.begin(), tagVec.end());
     const jintArray result = env->NewIntArray(length);
@@ -150,7 +150,7 @@
 static void Typeface_registerGenericFamily(JNIEnv *env, jobject, jstring familyName, jlong ptr) {
     ScopedUtfChars familyNameChars(env, familyName);
     minikin::SystemFonts::registerFallback(familyNameChars.c_str(),
-                                           toTypeface(ptr)->fFontCollection);
+                                           toTypeface(ptr)->getFontCollection());
 }
 
 #ifdef __ANDROID__
@@ -315,18 +315,19 @@
     std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections;
     std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex;
     for (Typeface* typeface : typefaces) {
-        bool inserted = fcToIndex.emplace(typeface->fFontCollection, fontCollections.size()).second;
+        bool inserted =
+                fcToIndex.emplace(typeface->getFontCollection(), fontCollections.size()).second;
         if (inserted) {
-            fontCollections.push_back(typeface->fFontCollection);
+            fontCollections.push_back(typeface->getFontCollection());
         }
     }
     minikin::FontCollection::writeVector(&writer, fontCollections);
     writer.write<uint32_t>(typefaces.size());
     for (Typeface* typeface : typefaces) {
-      writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second);
-      typeface->fStyle.writeTo(&writer);
-      writer.write<Typeface::Style>(typeface->fAPIStyle);
-      writer.write<int>(typeface->fBaseWeight);
+        writer.write<uint32_t>(fcToIndex.find(typeface->getFontCollection())->second);
+        typeface->getFontStyle().writeTo(&writer);
+        writer.write<Typeface::Style>(typeface->getAPIStyle());
+        writer.write<int>(typeface->getBaseWeight());
     }
     return static_cast<jint>(writer.size());
 }
@@ -349,12 +350,10 @@
     std::vector<jlong> faceHandles;
     faceHandles.reserve(typefaceCount);
     for (uint32_t i = 0; i < typefaceCount; i++) {
-        Typeface* typeface = new Typeface;
-        typeface->fFontCollection = fontCollections[reader.read<uint32_t>()];
-        typeface->fStyle = minikin::FontStyle(&reader);
-        typeface->fAPIStyle = reader.read<Typeface::Style>();
-        typeface->fBaseWeight = reader.read<int>();
-        typeface->fIsVariationInstance = false;
+        Typeface* typeface =
+                new Typeface(fontCollections[reader.read<uint32_t>()], minikin::FontStyle(&reader),
+                             reader.read<Typeface::Style>(), reader.read<int>(),
+                             false /* isVariationInstance */);
         faceHandles.push_back(toJLong(typeface));
     }
     const jlongArray result = env->NewLongArray(typefaceCount);
@@ -382,7 +381,8 @@
 
 // Critical Native
 static void Typeface_addFontCollection(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
-    std::shared_ptr<minikin::FontCollection> collection = toTypeface(faceHandle)->fFontCollection;
+    std::shared_ptr<minikin::FontCollection> collection =
+            toTypeface(faceHandle)->getFontCollection();
     minikin::SystemFonts::addFontMap(std::move(collection));
 }
 
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index d1782b2..7a4ae83 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -104,7 +104,7 @@
             } else {
                 fontId = fonts.size();  // This is new to us. Create new one.
                 std::shared_ptr<minikin::Font> font;
-                if (resolvedFace->fIsVariationInstance) {
+                if (resolvedFace->isVariationInstance()) {
                     // The optimization for target SDK 35 or before because the variation instance
                     // is already created and no runtime variation resolution happens on such
                     // environment.
diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp
index 93118aea..b51414f 100644
--- a/libs/hwui/tests/common/TestUtils.cpp
+++ b/libs/hwui/tests/common/TestUtils.cpp
@@ -183,8 +183,11 @@
 }
 
 SkFont TestUtils::defaultFont() {
-    const std::shared_ptr<minikin::MinikinFont>& minikinFont =
-      Typeface::resolveDefault(nullptr)->fFontCollection->getFamilyAt(0)->getFont(0)->baseTypeface();
+    const std::shared_ptr<minikin::MinikinFont>& minikinFont = Typeface::resolveDefault(nullptr)
+                                                                       ->getFontCollection()
+                                                                       ->getFamilyAt(0)
+                                                                       ->getFont(0)
+                                                                       ->baseTypeface();
     SkTypeface* skTypeface = reinterpret_cast<const MinikinFontSkia*>(minikinFont.get())->GetSkTypeface();
     LOG_ALWAYS_FATAL_IF(skTypeface == nullptr);
     return SkFont(sk_ref_sp(skTypeface));
diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp
index c71c4d2..7bcd937 100644
--- a/libs/hwui/tests/unit/TypefaceTests.cpp
+++ b/libs/hwui/tests/unit/TypefaceTests.cpp
@@ -90,40 +90,40 @@
 
 TEST(TypefaceTest, createWithDifferentBaseWeight) {
     std::unique_ptr<Typeface> bold(Typeface::createWithDifferentBaseWeight(nullptr, 700));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, bold->getAPIStyle());
 
     std::unique_ptr<Typeface> light(Typeface::createWithDifferentBaseWeight(nullptr, 300));
-    EXPECT_EQ(300, light->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, light->fAPIStyle);
+    EXPECT_EQ(300, light->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, light->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_fromRegular) {
     // In Java, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(nullptr, Typeface::kNormal));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(nullptr, Typeface::kBold));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.DEFAULT, Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(nullptr, Typeface::kItalic));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(Typeface::createRelative(nullptr, Typeface::kBoldItalic));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_BoldBase) {
@@ -132,31 +132,31 @@
     // In Java, Typeface.create(Typeface.create("sans-serif-bold"),
     // Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(700, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(700, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-bold"),
     // Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(1000, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(1000, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-bold"),
     // Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(700, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(700, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-bold"),
     // Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(1000, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(1000, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_LightBase) {
@@ -165,31 +165,31 @@
     // In Java, Typeface.create(Typeface.create("sans-serif-light"),
     // Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(300, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(300, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-light"),
     // Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(600, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(600, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-light"),
     // Typeface.ITLIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(300, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(300, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-light"),
     // Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(600, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(600, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_fromBoldStyled) {
@@ -198,32 +198,32 @@
     // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
     // Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
     // Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
     // Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
     // Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_fromItalicStyled) {
@@ -233,33 +233,33 @@
     // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
     // Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT,
     // Typeface.ITALIC), Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java,
     // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
     // Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
     // Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) {
@@ -270,27 +270,27 @@
     //     .setWeight(700).setItalic(false).build();
     // Typeface.create(typeface, Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java,
     // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
     //     .setWeight(700).setItalic(false).build();
     // Typeface.create(typeface, Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java,
     // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
     //     .setWeight(700).setItalic(false).build();
     // Typeface.create(typeface, Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
@@ -298,9 +298,9 @@
     // Typeface.create(typeface, Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createAbsolute) {
@@ -309,45 +309,45 @@
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(false)
     //     .build();
     std::unique_ptr<Typeface> regular(Typeface::createAbsolute(nullptr, 400, false));
-    EXPECT_EQ(400, regular->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+    EXPECT_EQ(400, regular->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(false)
     //     .build();
     std::unique_ptr<Typeface> bold(Typeface::createAbsolute(nullptr, 700, false));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(true)
     //     .build();
     std::unique_ptr<Typeface> italic(Typeface::createAbsolute(nullptr, 400, true));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(true)
     //     .build();
     std::unique_ptr<Typeface> boldItalic(Typeface::createAbsolute(nullptr, 700, true));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(1100).setItalic(true)
     //     .build();
     std::unique_ptr<Typeface> over1000(Typeface::createAbsolute(nullptr, 1100, false));
-    EXPECT_EQ(1000, over1000->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
+    EXPECT_EQ(1000, over1000->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle());
 }
 
 TEST(TypefaceTest, createFromFamilies_Single) {
@@ -355,43 +355,43 @@
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build();
     std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */));
-    EXPECT_EQ(400, regular->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+    EXPECT_EQ(400, regular->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
 
     // In Java, new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build();
     std::unique_ptr<Typeface> bold(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build();
     std::unique_ptr<Typeface> italic(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build();
     std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build();
     std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */));
-    EXPECT_EQ(1000, over1000->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
+    EXPECT_EQ(1000, over1000->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle());
 }
 
 TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) {
@@ -399,33 +399,33 @@
     std::unique_ptr<Typeface> regular(
             Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(400, regular->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+    EXPECT_EQ(400, regular->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
 
     // In Java, new Typeface.Builder("Family-Bold.ttf").build();
     std::unique_ptr<Typeface> bold(
             Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, new Typeface.Builder("Family-Italic.ttf").build();
     std::unique_ptr<Typeface> italic(
             Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build();
     std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
             makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
             nullptr /* fallback */));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createFromFamilies_Family) {
@@ -435,8 +435,8 @@
     std::unique_ptr<Typeface> typeface(
             Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(400, typeface->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
+    EXPECT_EQ(400, typeface->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant());
 }
 
 TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) {
@@ -445,8 +445,8 @@
     std::unique_ptr<Typeface> typeface(
             Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(700, typeface->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
+    EXPECT_EQ(700, typeface->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant());
 }
 
 TEST(TypefaceTest, createFromFamilies_Family_withFallback) {
@@ -458,8 +458,8 @@
     std::unique_ptr<Typeface> regular(
             Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, fallback.get()));
-    EXPECT_EQ(400, regular->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
+    EXPECT_EQ(400, regular->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
 }
 
 }  // namespace
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index bbb03e7..88981ea 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -961,8 +961,7 @@
      *
      * @hide
      */
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
-    public boolean isVisibleTo(String packageName) {
+    public boolean isVisibleTo(@NonNull String packageName) {
         return !mIsVisibilityRestricted
                 || TextUtils.equals(getProviderPackageName(), packageName)
                 || mAllowedPackages.contains(packageName);
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 3738312..e57148f 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -19,7 +19,6 @@
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
 import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES;
-import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME;
 import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL;
 import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
 import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;
@@ -1406,7 +1405,6 @@
         requestCreateController(controller, route, managerRequestId);
     }
 
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
     private List<MediaRoute2Info> getSortedRoutes(
             List<MediaRoute2Info> routes, List<String> packageOrder) {
         if (packageOrder.isEmpty()) {
@@ -1427,7 +1425,6 @@
     }
 
     @GuardedBy("mLock")
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
     private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked(
             List<MediaRoute2Info> routes) {
 
@@ -3654,7 +3651,6 @@
             }
         }
 
-        @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
         @Override
         public List<MediaRoute2Info> filterRoutesWithIndividualPreference(
                 List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) {
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 3854747..3f18eef 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -20,11 +20,9 @@
 import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME;
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -287,7 +285,6 @@
                 (route) -> sessionInfo.isSystemSession() ^ route.isSystemRoute());
     }
 
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
     private List<MediaRoute2Info> getSortedRoutes(RouteDiscoveryPreference preference) {
         if (!preference.shouldRemoveDuplicates()) {
             synchronized (mRoutesLock) {
@@ -311,7 +308,6 @@
         return routes;
     }
 
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
     private List<MediaRoute2Info> getFilteredRoutes(
             @NonNull RoutingSessionInfo sessionInfo,
             boolean includeSelectedRoutes,
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 4398b261..c48b5f4 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -11,6 +11,16 @@
 }
 
 flag {
+    name: "disable_set_bluetooth_ad2p_on_calls"
+    namespace: "media_better_together"
+    description: "Prevents calls to AudioService.setBluetoothA2dpOn(), known to cause incorrect audio routing to the built-in speakers."
+    bug: "294968421"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_audio_input_device_routing_and_volume_control"
     namespace: "media_better_together"
     description: "Allows audio input devices routing and volume control via system settings."
diff --git a/mime/Android.bp b/mime/Android.bp
index 20110f1..b609548 100644
--- a/mime/Android.bp
+++ b/mime/Android.bp
@@ -49,6 +49,17 @@
     ],
 }
 
+java_library {
+    name: "mimemap-testing-alt",
+    defaults: ["mimemap-defaults"],
+    static_libs: ["mimemap-testing-alt-res.jar"],
+    jarjar_rules: "jarjar-rules-alt.txt",
+    visibility: [
+        "//cts/tests/tests/mimemap:__subpackages__",
+        "//frameworks/base:__subpackages__",
+    ],
+}
+
 // The mimemap-res.jar and mimemap-testing-res.jar genrules produce a .jar that
 // has the resource file in a subdirectory res/ and testres/, respectively.
 // They need to be in different paths because one of them ends up in a
@@ -86,6 +97,19 @@
     cmd: "mkdir $(genDir)/testres/ && cp $(in) $(genDir)/testres/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres/",
 }
 
+// The same as mimemap-testing-res.jar except that the resources are placed in a different directory.
+// They get bundled with CTS so that CTS can compare a device's MimeMap implementation vs.
+// the stock Android one from when CTS was built.
+java_genrule {
+    name: "mimemap-testing-alt-res.jar",
+    tools: [
+        "soong_zip",
+    ],
+    srcs: [":mime.types.minimized-alt"],
+    out: ["mimemap-testing-alt-res.jar"],
+    cmd: "mkdir $(genDir)/testres-alt/ && cp $(in) $(genDir)/testres-alt/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres-alt/",
+}
+
 // Combination of all *mime.types.minimized resources.
 filegroup {
     name: "mime.types.minimized",
@@ -99,6 +123,19 @@
     ],
 }
 
+// Combination of all *mime.types.minimized resources.
+filegroup {
+    name: "mime.types.minimized-alt",
+    visibility: [
+        "//visibility:private",
+    ],
+    device_common_srcs: [
+        ":debian.mime.types.minimized-alt",
+        ":android.mime.types.minimized",
+        ":vendor.mime.types.minimized",
+    ],
+}
+
 java_genrule {
     name: "android.mime.types.minimized",
     visibility: [
diff --git a/mime/jarjar-rules-alt.txt b/mime/jarjar-rules-alt.txt
new file mode 100644
index 0000000..9a76443
--- /dev/null
+++ b/mime/jarjar-rules-alt.txt
@@ -0,0 +1 @@
+rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidAltMimeMapFactory
diff --git a/mime/jarjar-rules.txt b/mime/jarjar-rules.txt
index 145d1db..e1ea8e1 100644
--- a/mime/jarjar-rules.txt
+++ b/mime/jarjar-rules.txt
@@ -1 +1 @@
-rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory
\ No newline at end of file
+rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory
diff --git a/native/android/tests/system_health/OWNERS b/native/android/tests/system_health/OWNERS
new file mode 100644
index 0000000..e3bbee92
--- /dev/null
+++ b/native/android/tests/system_health/OWNERS
@@ -0,0 +1 @@
+include /ADPF_OWNERS
diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
index 068074a..8e52a00 100644
--- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
+++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
@@ -38,6 +38,7 @@
 import android.location.provider.ProviderProperties;
 import android.location.provider.ProviderRequest;
 import android.os.Bundle;
+import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
@@ -301,8 +302,13 @@
                             .setWorkSource(mRequest.getWorkSource())
                             .setHiddenFromAppOps(true)
                             .build();
-                    mLocationManager.requestLocationUpdates(mProvider, request,
-                            mContext.getMainExecutor(), this);
+
+                    try {
+                        mLocationManager.requestLocationUpdates(
+                                mProvider, request, mContext.getMainExecutor(), this);
+                    } catch (IllegalArgumentException e) {
+                        Log.e(TAG, "Failed to request location updates");
+                    }
                 }
             }
         }
@@ -311,7 +317,11 @@
             synchronized (mLock) {
                 int requestCode = mNextFlushCode++;
                 mPendingFlushes.put(requestCode, callback);
-                mLocationManager.requestFlush(mProvider, this, requestCode);
+                try {
+                    mLocationManager.requestFlush(mProvider, this, requestCode);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Failed to request flush");
+                }
             }
         }
 
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
index 2fac545..6fc6b54 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.ipc.ApiDescriptor
 import com.android.settingslib.ipc.ApiHandler
 import com.android.settingslib.ipc.ApiPermissionChecker
+import com.android.settingslib.metadata.PreferenceCoordinate
 import com.android.settingslib.metadata.PreferenceHierarchyNode
 import com.android.settingslib.metadata.PreferenceScreenRegistry
 
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
index ff14eb5..70ce62c 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
@@ -20,6 +20,7 @@
 import android.os.Parcel
 import com.android.settingslib.graph.proto.PreferenceProto
 import com.android.settingslib.ipc.MessageCodec
+import com.android.settingslib.metadata.PreferenceCoordinate
 import java.util.Arrays
 
 /** Message codec for [PreferenceGetterRequest]. */
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
similarity index 93%
rename from packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt
rename to packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
index 68aa2d2..2dd736a 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.graph
+package com.android.settingslib.metadata
 
 import android.os.Parcel
 import android.os.Parcelable
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index e1e1ee5..78d6c31 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -88,6 +88,7 @@
         matchAnyUserForAdmin: Boolean,
     ): List<ApplicationInfo> = try {
         coroutineScope {
+            // TODO(b/382016780): to be removed after flag cleanup.
             val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() }
             val hideWhenDisabledPackagesDeferred = async {
                 context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)
@@ -95,6 +96,7 @@
             val installedApplicationsAsUser =
                 getInstalledApplications(userId, matchAnyUserForAdmin)
 
+            // TODO(b/382016780): to be removed after flag cleanup.
             val hiddenSystemModules = hiddenSystemModulesDeferred.await()
             val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await()
             installedApplicationsAsUser.filter { app ->
@@ -206,6 +208,7 @@
     private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean =
         app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages
 
+    // TODO(b/382016780): to be removed after flag cleanup.
     private fun PackageManager.getHiddenSystemModules(): Set<String> {
         val moduleInfos = getInstalledModules(0).filter { it.isHidden }
         val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet()
@@ -218,13 +221,14 @@
     companion object {
         private const val TAG = "AppListRepository"
 
+        // TODO(b/382016780): to be removed after flag cleanup.
         private fun ApplicationInfo.isInAppList(
             showInstantApps: Boolean,
             hiddenSystemModules: Set<String>,
             hideWhenDisabledPackages: Array<String>,
         ) = when {
             !showInstantApps && isInstantApp -> false
-            packageName in hiddenSystemModules -> false
+            !Flags.removeHiddenModuleUsage() && (packageName in hiddenSystemModules) -> false
             packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed
             enabled -> true
             else -> enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index b1baa86..fd4b189 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -281,6 +281,23 @@
         )
     }
 
+    @EnableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
+    @Test
+    fun loadApps_shouldIncludeAllSystemModuleApps() = runTest {
+        packageManager.stub {
+            on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
+        }
+        mockInstalledApplications(
+            listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP),
+            ADMIN_USER_ID
+        )
+
+        val appList = repository.loadApps(userId = ADMIN_USER_ID)
+
+        assertThat(appList).containsExactly(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP)
+    }
+
+    @DisableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
     @EnableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
     @Test
     fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest {
@@ -297,7 +314,7 @@
         assertThat(appList).containsExactly(NORMAL_APP)
     }
 
-    @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+    @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX, Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
     @Test
     fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest {
         packageManager.stub {
@@ -456,6 +473,7 @@
             isArchived = true
         }
 
+        // TODO(b/382016780): to be removed after flag cleanup.
         val HIDDEN_APEX_APP = ApplicationInfo().apply {
             packageName = "hidden.apex.package"
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
index c482995..3390296 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
@@ -137,6 +137,7 @@
 
     /**
      * Returns a boolean indicating whether the given package is a hidden system module
+     * TODO(b/382016780): to be removed after flag cleanup.
      */
     public static boolean isHiddenSystemModule(Context context, String packageName) {
         return ApplicationsState.getInstance((Application) context.getApplicationContext())
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index fd9a008..4110d53 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -157,6 +157,7 @@
     int mCurComputingSizeUserId;
     boolean mSessionsChanged;
     // Maps all installed modules on the system to whether they're hidden or not.
+    // TODO(b/382016780): to be removed after flag cleanup.
     final HashMap<String, Boolean> mSystemModules = new HashMap<>();
 
     // Temporary for dispatching session callbacks.  Only touched by main thread.
@@ -226,12 +227,14 @@
         mRetrieveFlags = PackageManager.MATCH_DISABLED_COMPONENTS |
                 PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
 
-        final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */);
-        for (ModuleInfo info : moduleInfos) {
-            mSystemModules.put(info.getPackageName(), info.isHidden());
-            if (Flags.provideInfoOfApkInApex()) {
-                for (String apkInApexPackageName : info.getApkInApexPackageNames()) {
-                    mSystemModules.put(apkInApexPackageName, info.isHidden());
+        if (!Flags.removeHiddenModuleUsage()) {
+            final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */);
+            for (ModuleInfo info : moduleInfos) {
+                mSystemModules.put(info.getPackageName(), info.isHidden());
+                if (Flags.provideInfoOfApkInApex()) {
+                    for (String apkInApexPackageName : info.getApkInApexPackageNames()) {
+                        mSystemModules.put(apkInApexPackageName, info.isHidden());
+                    }
                 }
             }
         }
@@ -336,7 +339,7 @@
                 }
                 mHaveDisabledApps = true;
             }
-            if (isHiddenModule(info.packageName)) {
+            if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) {
                 mApplications.remove(i--);
                 continue;
             }
@@ -453,6 +456,7 @@
         return mHaveInstantApps;
     }
 
+    // TODO(b/382016780): to be removed after flag cleanup.
     boolean isHiddenModule(String packageName) {
         Boolean isHidden = mSystemModules.get(packageName);
         if (isHidden == null) {
@@ -462,6 +466,7 @@
         return isHidden;
     }
 
+    // TODO(b/382016780): to be removed after flag cleanup.
     boolean isSystemModule(String packageName) {
         return mSystemModules.containsKey(packageName);
     }
@@ -755,7 +760,7 @@
             Log.i(TAG, "Looking up entry of pkg " + info.packageName + ": " + entry);
         }
         if (entry == null) {
-            if (isHiddenModule(info.packageName)) {
+            if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) {
                 if (DEBUG) {
                     Log.i(TAG, "No AppEntry for " + info.packageName + " (hidden module)");
                 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index b2c2794..e05f0a1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -483,14 +483,18 @@
 
     void onActiveDeviceChanged(CachedBluetoothDevice device) {
         if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
-            if (device.isConnectedHearingAidDevice()) {
+            if (device.isConnectedHearingAidDevice()
+                    && (device.isActiveDevice(BluetoothProfile.HEARING_AID)
+                    || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) {
                 setAudioRoutingConfig(device);
             } else {
                 clearAudioRoutingConfig();
             }
         }
         if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
-            if (device.isConnectedHearingAidDevice()) {
+            if (device.isConnectedHearingAidDevice()
+                    && (device.isActiveDevice(BluetoothProfile.HEARING_AID)
+                    || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) {
                 setMicrophoneForCalls(device);
             } else {
                 clearMicrophoneForCalls();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
index 3b18aa3..4e821ca 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.applications;
 
+import static android.content.pm.Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE;
 import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX;
 import static android.os.UserHandle.MU_ENABLED;
 import static android.os.UserHandle.USER_SYSTEM;
@@ -59,6 +60,8 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.text.TextUtils;
 import android.util.IconDrawableFactory;
@@ -204,6 +207,7 @@
             info.setPackageName(packageName);
             info.setApkInApexPackageNames(Collections.singletonList(apexPackageName));
             // will treat any app with package name that contains "hidden" as hidden module
+            // TODO(b/382016780): to be removed after flag cleanup.
             info.setHidden(!TextUtils.isEmpty(packageName) && packageName.contains("hidden"));
             return info;
         }
@@ -414,6 +418,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
     public void onResume_shouldNotIncludeSystemHiddenModule() {
         mSession.onResume();
 
@@ -424,6 +429,18 @@
     }
 
     @Test
+    @EnableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
+    public void onResume_shouldIncludeSystemModule() {
+        mSession.onResume();
+
+        final List<ApplicationInfo> mApplications = mApplicationsState.mApplications;
+        assertThat(mApplications).hasSize(3);
+        assertThat(mApplications.get(0).packageName).isEqualTo("test.package.1");
+        assertThat(mApplications.get(1).packageName).isEqualTo("test.hidden.module.2");
+        assertThat(mApplications.get(2).packageName).isEqualTo("test.package.3");
+    }
+
+    @Test
     public void removeAndInstall_noWorkprofile_doResumeIfNeededLocked_shouldClearEntries()
             throws RemoteException {
         // scenario: only owner user
@@ -832,6 +849,7 @@
         mApplicationsState.mEntriesMap.clear();
         ApplicationInfo appInfo = createApplicationInfo(PKG_1, /* uid= */ 0);
         mApplicationsState.mApplications.add(appInfo);
+        // TODO(b/382016780): to be removed after flag cleanup.
         mApplicationsState.mSystemModules.put(PKG_1, /* value= */ false);
 
         assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName)
@@ -839,6 +857,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
     public void isHiddenModule_hasApkInApexInfo_shouldSupportHiddenApexPackage() {
         mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
         ApplicationsState.sInstance = null;
@@ -853,6 +872,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
     public void isHiddenModule_noApkInApexInfo_onlySupportHiddenModule() {
         mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
         ApplicationsState.sInstance = null;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index 21dde1f..a215464 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -50,6 +50,9 @@
 import android.media.AudioManager;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.os.Parcel;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.FeatureFlagUtils;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -72,6 +75,8 @@
 public class HearingAidDeviceManagerTest {
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final long HISYNCID1 = 10;
     private static final long HISYNCID2 = 11;
@@ -736,6 +741,7 @@
 
     @Test
     public void onActiveDeviceChanged_connected_callSetStrategies() {
+        when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
         when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn(
                 mHearingDeviceAttribute);
         when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true);
@@ -750,6 +756,7 @@
 
     @Test
     public void onActiveDeviceChanged_disconnected_callSetStrategiesWithAutoValue() {
+        when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(false);
         when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn(
                 mHearingDeviceAttribute);
         when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false);
@@ -952,6 +959,38 @@
                 ConnectionStatus.CONNECTED);
     }
 
+    @Test
+    @RequiresFlagsEnabled(
+            com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL)
+    public void onActiveDeviceChanged_activeHearingAidProfile_callSetInputDeviceForCalls() {
+        when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true);
+        when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true);
+        doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(),
+                anyInt());
+
+        mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1);
+
+        verify(mHelper).setPreferredInputDeviceForCalls(
+                eq(mCachedDevice1), eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO));
+
+    }
+
+    @Test
+    @RequiresFlagsEnabled(
+            com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL)
+    public void onActiveDeviceChanged_notActiveHearingAidProfile_callClearInputDeviceForCalls() {
+        when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false);
+        when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true);
+        doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(),
+                anyInt());
+
+        mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1);
+
+        verify(mHelper).clearPreferredInputDeviceForCalls();
+    }
+
     private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) {
         return new HearingAidInfo.Builder()
                 .setAshaDeviceSide(HearingAidInfo.DeviceSide.SIDE_LEFT)
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 9adc95a..6b2449f 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -92,7 +92,6 @@
         "tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt",
         "tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt",
         "tests/src/**/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt",
-        "tests/src/**/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt",
         "tests/src/**/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt",
         "tests/src/**/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt",
         "tests/src/**/systemui/education/domain/ui/view/ContextualEduDialogTest.kt",
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 c7d6e8a..96401ce 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
@@ -147,21 +147,66 @@
     private val orientation: Orientation,
     private val overscrollEffect: OverscrollEffect?,
     private val enabled: Boolean,
-) : ModifierNodeElement<NestedDraggableNode>() {
-    override fun create(): NestedDraggableNode {
-        return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled)
+) : ModifierNodeElement<NestedDraggableRootNode>() {
+    override fun create(): NestedDraggableRootNode {
+        return NestedDraggableRootNode(draggable, orientation, overscrollEffect, enabled)
     }
 
-    override fun update(node: NestedDraggableNode) {
+    override fun update(node: NestedDraggableRootNode) {
         node.update(draggable, orientation, overscrollEffect, enabled)
     }
 }
 
+/**
+ * A root node on top of [NestedDraggableNode] so that no [PointerInputModifierNode] is installed
+ * when this draggable is disabled.
+ */
+private class NestedDraggableRootNode(
+    draggable: NestedDraggable,
+    orientation: Orientation,
+    overscrollEffect: OverscrollEffect?,
+    enabled: Boolean,
+) : DelegatingNode() {
+    private var delegateNode =
+        if (enabled) create(draggable, orientation, overscrollEffect) else null
+
+    fun update(
+        draggable: NestedDraggable,
+        orientation: Orientation,
+        overscrollEffect: OverscrollEffect?,
+        enabled: Boolean,
+    ) {
+        // Disabled.
+        if (!enabled) {
+            delegateNode?.let { undelegate(it) }
+            delegateNode = null
+            return
+        }
+
+        // Disabled => Enabled.
+        val nullableDelegate = delegateNode
+        if (nullableDelegate == null) {
+            delegateNode = create(draggable, orientation, overscrollEffect)
+            return
+        }
+
+        // Enabled => Enabled (update).
+        nullableDelegate.update(draggable, orientation, overscrollEffect)
+    }
+
+    private fun create(
+        draggable: NestedDraggable,
+        orientation: Orientation,
+        overscrollEffect: OverscrollEffect?,
+    ): NestedDraggableNode {
+        return delegate(NestedDraggableNode(draggable, orientation, overscrollEffect))
+    }
+}
+
 private class NestedDraggableNode(
     private var draggable: NestedDraggable,
     override var orientation: Orientation,
     private var overscrollEffect: OverscrollEffect?,
-    private var enabled: Boolean,
 ) :
     DelegatingNode(),
     PointerInputModifierNode,
@@ -169,23 +214,11 @@
     CompositionLocalConsumerModifierNode,
     OrientationAware {
     private val nestedScrollDispatcher = NestedScrollDispatcher()
-    private var trackWheelScroll: SuspendingPointerInputModifierNode? = null
-        set(value) {
-            field?.let { undelegate(it) }
-            field = value?.also { delegate(it) }
-        }
-
-    private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null
-        set(value) {
-            field?.let { undelegate(it) }
-            field = value?.also { delegate(it) }
-        }
-
-    private var detectDragsDelegate: SuspendingPointerInputModifierNode? = null
-        set(value) {
-            field?.let { undelegate(it) }
-            field = value?.also { delegate(it) }
-        }
+    private val trackWheelScroll =
+        delegate(SuspendingPointerInputModifierNode { trackWheelScroll() })
+    private val trackDownPositionDelegate =
+        delegate(SuspendingPointerInputModifierNode { trackDownPosition() })
+    private val detectDragsDelegate = delegate(SuspendingPointerInputModifierNode { detectDrags() })
 
     /** The controller created by the nested scroll logic (and *not* the drag logic). */
     private var nestedScrollController: NestedScrollController? = null
@@ -214,26 +247,25 @@
         draggable: NestedDraggable,
         orientation: Orientation,
         overscrollEffect: OverscrollEffect?,
-        enabled: Boolean,
     ) {
+        if (
+            draggable == this.draggable &&
+                orientation == this.orientation &&
+                overscrollEffect == this.overscrollEffect
+        ) {
+            return
+        }
+
         this.draggable = draggable
         this.orientation = orientation
         this.overscrollEffect = overscrollEffect
-        this.enabled = enabled
 
-        trackDownPositionDelegate?.resetPointerInputHandler()
-        detectDragsDelegate?.resetPointerInputHandler()
+        trackWheelScroll.resetPointerInputHandler()
+        trackDownPositionDelegate.resetPointerInputHandler()
+        detectDragsDelegate.resetPointerInputHandler()
+
         nestedScrollController?.ensureOnDragStoppedIsCalled()
         nestedScrollController = null
-
-        if (!enabled && trackWheelScroll != null) {
-            check(trackDownPositionDelegate != null)
-            check(detectDragsDelegate != null)
-
-            trackWheelScroll = null
-            trackDownPositionDelegate = null
-            detectDragsDelegate = null
-        }
     }
 
     override fun onPointerEvent(
@@ -241,26 +273,15 @@
         pass: PointerEventPass,
         bounds: IntSize,
     ) {
-        if (!enabled) return
-
-        if (trackWheelScroll == null) {
-            check(trackDownPositionDelegate == null)
-            check(detectDragsDelegate == null)
-
-            trackWheelScroll = SuspendingPointerInputModifierNode { trackWheelScroll() }
-            trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() }
-            detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() }
-        }
-
-        checkNotNull(trackWheelScroll).onPointerEvent(pointerEvent, pass, bounds)
-        checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds)
-        checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds)
+        trackWheelScroll.onPointerEvent(pointerEvent, pass, bounds)
+        trackDownPositionDelegate.onPointerEvent(pointerEvent, pass, bounds)
+        detectDragsDelegate.onPointerEvent(pointerEvent, pass, bounds)
     }
 
     override fun onCancelPointerInput() {
-        trackWheelScroll?.onCancelPointerInput()
-        trackDownPositionDelegate?.onCancelPointerInput()
-        detectDragsDelegate?.onCancelPointerInput()
+        trackWheelScroll.onCancelPointerInput()
+        trackDownPositionDelegate.onCancelPointerInput()
+        detectDragsDelegate.onCancelPointerInput()
     }
 
     /*
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 f9cf495..5de0f12 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
@@ -25,11 +25,14 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@@ -37,10 +40,14 @@
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.PointerType
 import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ScrollWheel
+import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
@@ -693,6 +700,7 @@
     }
 
     @Test
+    @Ignore("b/388507816: re-enable this when the crash in HitPath is fixed")
     fun pointersDown_clearedWhenDisabled() {
         val draggable = TestDraggable()
         var enabled by mutableStateOf(true)
@@ -740,6 +748,31 @@
         assertThat(draggable.onDragStartedCalled).isFalse()
     }
 
+    @Test
+    fun doesNotConsumeGesturesWhenDisabled() {
+        val buttonTag = "button"
+        rule.setContent {
+            Box {
+                var count by remember { mutableStateOf(0) }
+                Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) {
+                    Text("Count: $count")
+                }
+
+                Box(
+                    Modifier.fillMaxSize()
+                        .nestedDraggable(remember { TestDraggable() }, orientation, enabled = false)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0")
+
+        // Click on the root at its center, where the button is located. Clicks should go through
+        // the draggable and reach the button given that it is disabled.
+        repeat(3) { rule.onRoot().performClick() }
+        rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3")
+    }
+
     private fun ComposeContentTestRule.setContentWithTouchSlop(
         content: @Composable () -> Unit
     ): Float {
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 b41c558..2ca8464 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
@@ -429,6 +429,58 @@
     }
 
     /**
+     * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
+     * Prioritizes actions with matching [Swipe.Resolved.fromSource].
+     *
+     * @param swipe The swipe to match against.
+     * @return The best matching [UserActionResult], or `null` if no match is found.
+     */
+    private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
+        if (!areSwipesAllowed()) {
+            return null
+        }
+
+        var bestPoints = Int.MIN_VALUE
+        var bestMatch: UserActionResult? = null
+        userActions.forEach { (actionSwipe, actionResult) ->
+            if (
+                actionSwipe !is Swipe.Resolved ||
+                    // The direction must match.
+                    actionSwipe.direction != swipe.direction ||
+                    // The number of pointers down must match.
+                    actionSwipe.pointerCount != swipe.pointerCount ||
+                    // The action requires a specific fromSource.
+                    (actionSwipe.fromSource != null &&
+                        actionSwipe.fromSource != swipe.fromSource) ||
+                    // The action requires a specific pointerType.
+                    (actionSwipe.pointersType != null &&
+                        actionSwipe.pointersType != swipe.pointersType)
+            ) {
+                // This action is not eligible.
+                return@forEach
+            }
+
+            val sameFromSource = actionSwipe.fromSource == swipe.fromSource
+            val samePointerType = actionSwipe.pointersType == swipe.pointersType
+            // Prioritize actions with a perfect match.
+            if (sameFromSource && samePointerType) {
+                return actionResult
+            }
+
+            var points = 0
+            if (sameFromSource) points++
+            if (samePointerType) points++
+
+            // Otherwise, keep track of the best eligible action.
+            if (points > bestPoints) {
+                bestPoints = points
+                bestMatch = actionResult
+            }
+        }
+        return bestMatch
+    }
+
+    /**
      * Update the swipes results.
      *
      * Usually we don't want to update them while doing a drag, because this could change the target
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 3f6bce7..e221211 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
@@ -37,11 +37,7 @@
     draggableHandler: DraggableHandlerImpl,
     swipeDetector: SwipeDetector,
 ): Modifier {
-    return if (draggableHandler.enabled()) {
-        this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
-    } else {
-        this
-    }
+    return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled()))
 }
 
 private fun DraggableHandlerImpl.enabled(): Boolean {
@@ -61,84 +57,62 @@
     return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
 }
 
-/**
- * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
- * Prioritizes actions with matching [Swipe.Resolved.fromSource].
- *
- * @param swipe The swipe to match against.
- * @return The best matching [UserActionResult], or `null` if no match is found.
- */
-internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
-    if (!areSwipesAllowed()) {
-        return null
-    }
-
-    var bestPoints = Int.MIN_VALUE
-    var bestMatch: UserActionResult? = null
-    userActions.forEach { (actionSwipe, actionResult) ->
-        if (
-            actionSwipe !is Swipe.Resolved ||
-                // The direction must match.
-                actionSwipe.direction != swipe.direction ||
-                // The number of pointers down must match.
-                actionSwipe.pointerCount != swipe.pointerCount ||
-                // The action requires a specific fromSource.
-                (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) ||
-                // The action requires a specific pointerType.
-                (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType)
-        ) {
-            // This action is not eligible.
-            return@forEach
-        }
-
-        val sameFromSource = actionSwipe.fromSource == swipe.fromSource
-        val samePointerType = actionSwipe.pointersType == swipe.pointersType
-        // Prioritize actions with a perfect match.
-        if (sameFromSource && samePointerType) {
-            return actionResult
-        }
-
-        var points = 0
-        if (sameFromSource) points++
-        if (samePointerType) points++
-
-        // Otherwise, keep track of the best eligible action.
-        if (points > bestPoints) {
-            bestPoints = points
-            bestMatch = actionResult
-        }
-    }
-    return bestMatch
-}
-
 private data class SwipeToSceneElement(
     val draggableHandler: DraggableHandlerImpl,
     val swipeDetector: SwipeDetector,
+    val enabled: Boolean,
 ) : ModifierNodeElement<SwipeToSceneRootNode>() {
     override fun create(): SwipeToSceneRootNode =
-        SwipeToSceneRootNode(draggableHandler, swipeDetector)
+        SwipeToSceneRootNode(draggableHandler, swipeDetector, enabled)
 
     override fun update(node: SwipeToSceneRootNode) {
-        node.update(draggableHandler, swipeDetector)
+        node.update(draggableHandler, swipeDetector, enabled)
     }
 }
 
 private class SwipeToSceneRootNode(
     draggableHandler: DraggableHandlerImpl,
     swipeDetector: SwipeDetector,
+    enabled: Boolean,
 ) : DelegatingNode() {
-    private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+    private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null
 
-    fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) {
-        if (draggableHandler == delegateNode.draggableHandler) {
+    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.
-            delegateNode.swipeDetector = swipeDetector
+            nullableDelegate.swipeDetector = swipeDetector
         } else {
             // The draggableHandler changed, force recreate the underlying SwipeToSceneNode.
-            undelegate(delegateNode)
-            delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+            undelegate(nullableDelegate)
+            delegateNode = create(draggableHandler, swipeDetector)
         }
     }
+
+    private fun create(
+        draggableHandler: DraggableHandlerImpl,
+        swipeDetector: SwipeDetector,
+    ): SwipeToSceneNode {
+        return delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+    }
 }
 
 private class SwipeToSceneNode(
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 9135fdd..e80805a 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
@@ -936,4 +936,45 @@
         assertThat(state.transitionState).isIdle()
         assertThat(state.transitionState).hasCurrentScene(SceneC)
     }
+
+    @Test
+    fun swipeToSceneNodeIsKeptWhenDisabled() {
+        var hasHorizontalActions by mutableStateOf(false)
+        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state) {
+                scene(
+                    SceneA,
+                    userActions =
+                        buildList {
+                                add(Swipe.Down to SceneB)
+
+                                if (hasHorizontalActions) {
+                                    add(Swipe.Left to SceneC)
+                                }
+                            }
+                            .toMap(),
+                ) {
+                    Box(Modifier.fillMaxSize())
+                }
+                scene(SceneB) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        // Swipe down to start a transition to B.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(0f, touchSlop))
+        }
+
+        assertThat(state.transitionState).isSceneTransition()
+
+        // Add new horizontal user actions. This should not stop the current transition, even if a
+        // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was.
+        hasHorizontalActions = true
+        rule.waitForIdle()
+        assertThat(state.transitionState).isSceneTransition()
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
index a11dace..4c329dc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
@@ -18,12 +18,20 @@
 
 import android.bluetooth.BluetoothLeBroadcast
 import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.content.ContentResolver
+import android.content.applicationContext
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.BluetoothEventManager
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.settingslib.bluetooth.VolumeControlProfile
+import com.android.settingslib.volume.shared.AudioSharingLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -38,10 +46,16 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -50,8 +64,11 @@
 class AudioSharingInteractorTest : SysuiTestCase() {
     @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
     private val kosmos = testKosmos()
+
     @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+
     @Mock private lateinit var bluetoothLeBroadcastMetadata: BluetoothLeBroadcastMetadata
+
     @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
     private lateinit var underTest: AudioSharingInteractor
 
@@ -157,13 +174,15 @@
     fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() =
         with(kosmos) {
             testScope.runTest {
-                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                 bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                 bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                     localBluetoothLeBroadcast
                 )
                 val job = launch { underTest.handleAudioSourceWhenReady() }
                 runCurrent()
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+                runCurrent()
 
                 assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
                 job.cancel()
@@ -174,15 +193,14 @@
     fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() =
         with(kosmos) {
             testScope.runTest {
-                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                 bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                 bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                     localBluetoothLeBroadcast
                 )
                 val job = launch { underTest.handleAudioSourceWhenReady() }
                 runCurrent()
-                verify(localBluetoothLeBroadcast)
-                    .registerServiceCallBack(any(), callbackCaptor.capture())
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                 runCurrent()
 
                 assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
@@ -194,13 +212,15 @@
     fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() =
         with(kosmos) {
             testScope.runTest {
-                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                 bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                 bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                     localBluetoothLeBroadcast
                 )
                 val job = launch { underTest.handleAudioSourceWhenReady() }
                 runCurrent()
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                runCurrent()
                 verify(localBluetoothLeBroadcast)
                     .registerServiceCallBack(any(), callbackCaptor.capture())
                 runCurrent()
@@ -211,4 +231,100 @@
                 job.cancel()
             }
         }
+
+    @Test
+    fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                val (broadcast, repository) = setupRepositoryImpl()
+                val interactor =
+                    object :
+                        AudioSharingInteractorImpl(
+                            applicationContext,
+                            localBluetoothManager,
+                            repository,
+                            testDispatcher,
+                        ) {
+                        override suspend fun audioSharingAvailable() = true
+                    }
+                val job = launch { interactor.handleAudioSourceWhenReady() }
+                runCurrent()
+                // Verify callback registered for onBroadcastStartedOrStopped
+                verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
+                runCurrent()
+                // Verify source is not added
+                verify(repository, never()).addSource()
+                job.cancel()
+            }
+        }
+
+    @Test
+    fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                val (broadcast, repository) = setupRepositoryImpl()
+                val interactor =
+                    object :
+                        AudioSharingInteractorImpl(
+                            applicationContext,
+                            localBluetoothManager,
+                            repository,
+                            testDispatcher,
+                        ) {
+                        override suspend fun audioSharingAvailable() = true
+                    }
+                val job = launch { interactor.handleAudioSourceWhenReady() }
+                runCurrent()
+                // Verify callback registered for onBroadcastStartedOrStopped
+                verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
+                // Audio sharing started, trigger onBroadcastStarted
+                whenever(broadcast.isEnabled(null)).thenReturn(true)
+                callbackCaptor.value.onBroadcastStarted(0, 0)
+                runCurrent()
+                // Verify callback registered for onBroadcastMetadataChanged
+                verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture())
+                runCurrent()
+                // Trigger onBroadcastMetadataChanged (ready to add source)
+                callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata)
+                runCurrent()
+                // Verify source added
+                verify(repository).addSource()
+                job.cancel()
+            }
+        }
+
+    private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> {
+        with(kosmos) {
+            val broadcast =
+                mock<LocalBluetoothLeBroadcast> {
+                    on { isProfileReady } doReturn true
+                    on { isEnabled(null) } doReturn false
+                }
+            val assistant =
+                mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true }
+            val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true }
+            val profileManager =
+                mock<LocalBluetoothProfileManager> {
+                    on { leAudioBroadcastProfile } doReturn broadcast
+                    on { leAudioBroadcastAssistantProfile } doReturn assistant
+                    on { volumeControlProfile } doReturn volumeControl
+                }
+            whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+            whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {})
+
+            val repository =
+                AudioSharingRepositoryImpl(
+                    localBluetoothManager,
+                    com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl(
+                        mock<ContentResolver> {},
+                        localBluetoothManager,
+                        testScope.backgroundScope,
+                        testScope.testScheduler,
+                        mock<AudioSharingLogger> {},
+                    ),
+                    testDispatcher,
+                )
+            return Pair(broadcast, spy(repository))
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
index acfe9dd..f074606 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
@@ -111,6 +111,28 @@
         }
 
     @Test
+    fun testStopAudioSharing() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+                audioSharingRepository.setAudioSharingAvailable(true)
+                underTest.stopAudioSharing()
+                verify(leAudioBroadcastProfile).stopLatestBroadcast()
+            }
+        }
+
+    @Test
+    fun testStopAudioSharing_flagOff_doNothing() =
+        with(kosmos) {
+            testScope.runTest {
+                audioSharingRepository.setAudioSharingAvailable(false)
+                underTest.stopAudioSharing()
+                verify(leAudioBroadcastProfile, never()).stopLatestBroadcast()
+            }
+        }
+
+    @Test
     fun testAddSource_flagOff_doesNothing() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
index 44f9720..ad0337e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
@@ -15,14 +15,15 @@
  */
 package com.android.systemui.bluetooth.qsdialog
 
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
@@ -48,6 +49,7 @@
     private lateinit var notConnectedDeviceItem: DeviceItem
     private lateinit var connectedMediaDeviceItem: DeviceItem
     private lateinit var connectedOtherDeviceItem: DeviceItem
+    private lateinit var audioSharingDeviceItem: DeviceItem
     @Mock private lateinit var dialog: SystemUIDialog
 
     @Before
@@ -59,7 +61,7 @@
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
-                background = null
+                background = null,
             )
         notConnectedDeviceItem =
             DeviceItem(
@@ -68,7 +70,7 @@
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
-                background = null
+                background = null,
             )
         connectedMediaDeviceItem =
             DeviceItem(
@@ -77,7 +79,7 @@
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
-                background = null
+                background = null,
             )
         connectedOtherDeviceItem =
             DeviceItem(
@@ -86,7 +88,16 @@
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
-                background = null
+                background = null,
+            )
+        audioSharingDeviceItem =
+            DeviceItem(
+                type = DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+                deviceName = DEVICE_NAME,
+                connectionSummary = DEVICE_CONNECTION_SUMMARY,
+                iconWithDescription = null,
+                background = null,
             )
         actionInteractorImpl = kosmos.deviceItemActionInteractorImpl
     }
@@ -135,6 +146,29 @@
         }
     }
 
+    @Test
+    fun onActionIconClick_onIntent() {
+        with(kosmos) {
+            testScope.runTest {
+                var onIntentCalledOnAddress = ""
+                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
+                actionInteractorImpl.onActionIconClick(connectedMediaDeviceItem) {
+                    onIntentCalledOnAddress = connectedMediaDeviceItem.cachedBluetoothDevice.address
+                }
+                assertThat(onIntentCalledOnAddress).isEqualTo(DEVICE_ADDRESS)
+            }
+        }
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun onActionIconClick_audioSharingDeviceType_throwException() {
+        with(kosmos) {
+            testScope.runTest {
+                actionInteractorImpl.onActionIconClick(audioSharingDeviceItem) {}
+            }
+        }
+    }
+
     private companion object {
         const val DEVICE_NAME = "device"
         const val DEVICE_CONNECTION_SUMMARY = "active"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
index 20d6615..6c955bf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
@@ -256,6 +256,22 @@
 
     @EnableSceneContainer
     @Test
+    fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() =
+        testScope.runTest {
+            underTest = kosmos.deviceEntryHapticsInteractor
+            val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+
+            enrollFace()
+            kosmos.configureKeyguardBypass(isBypassAvailable = false)
+            runCurrent()
+            configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false)
+            kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true
+
+            assertThat(playSuccessHaptic).isNotNull()
+        }
+
+    @EnableSceneContainer
+    @Test
     fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() =
         testScope.runTest {
             kosmos.configureKeyguardBypass(isBypassAvailable = false)
@@ -299,6 +315,7 @@
     private fun configureDeviceEntryFromBiometricSource(
         isFpUnlock: Boolean = false,
         isFaceUnlock: Boolean = false,
+        bypassEnabled: Boolean = true,
     ) {
         // Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState
         if (isFpUnlock) {
@@ -314,11 +331,14 @@
             )
 
             // Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING
-            kosmos.sceneInteractor.setTransitionState(
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(Scenes.Lockscreen)
+            // if the successful face authentication will bypass keyguard
+            if (bypassEnabled) {
+                kosmos.sceneInteractor.setTransitionState(
+                    MutableStateFlow<ObservableTransitionState>(
+                        ObservableTransitionState.Idle(Scenes.Lockscreen)
+                    )
                 )
-            )
+            }
         }
         underTest = kosmos.deviceEntryHapticsInteractor
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
index 74e8257..5e023a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
@@ -292,8 +292,7 @@
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue + 1)
         }
@@ -306,8 +305,7 @@
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue)
         }
@@ -321,8 +319,7 @@
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue + 1)
         }
@@ -335,8 +332,7 @@
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue)
         }
@@ -347,8 +343,7 @@
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             assertThat(model?.lastShortcutTriggeredTime).isNull()
 
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
 
             assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
         }
@@ -358,15 +353,14 @@
         testScope.runTest {
             setUpForDeviceConnection()
             tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
-                DeviceType.TOUCHPAD,
+                getTargetDevice(gestureType),
                 eduClock.instant(),
             )
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
             eduClock.offset(initialDelayElapsedDuration)
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue + 1)
         }
@@ -376,33 +370,92 @@
         testScope.runTest {
             setUpForDeviceConnection()
             tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
-                DeviceType.TOUCHPAD,
+                getTargetDevice(gestureType),
                 eduClock.instant(),
             )
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
             // No offset to the clock to simulate update before initial delay
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue)
         }
 
     @Test
-    fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() =
+    fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchOrNotifyTime() =
         testScope.runTest {
-            // No update to OOBE launch time to simulate no OOBE is launched yet
+            // No update to OOBE launch/notify time to simulate no OOBE is launched yet
             setUpForDeviceConnection()
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue)
         }
 
+    @Test
+    fun dataUpdatedOnIncrementSignalCountAfterNotifyTimeDelayWithoutLaunchTime() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            tutorialSchedulerRepository.setNotifiedTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            eduClock.offset(initialDelayElapsedDuration)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
+    @Test
+    fun dataUnchangedOnIncrementSignalCountBeforeLaunchTimeDelayWithNotifyTime() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            tutorialSchedulerRepository.setNotifiedTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+            eduClock.offset(initialDelayElapsedDuration)
+
+            tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            // No offset to the clock to simulate update before initial delay of launch time
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue)
+        }
+
+    @Test
+    fun dataUpdatedOnIncrementSignalCountAfterLaunchTimeDelayWithNotifyTime() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            tutorialSchedulerRepository.setNotifiedTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+            eduClock.offset(initialDelayElapsedDuration)
+
+            tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            eduClock.offset(initialDelayElapsedDuration)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
     private suspend fun setUpForInitialDelayElapse() {
         tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
             DeviceType.TOUCHPAD,
@@ -465,12 +518,18 @@
         keyboardRepository.setIsAnyKeyboardConnected(true)
     }
 
-    private fun getOverviewProxyListener(): OverviewProxyListener {
+    private fun updateContextualEduStats(isTrackpadGesture: Boolean, gestureType: GestureType) {
         val listenerCaptor = argumentCaptor<OverviewProxyListener>()
         verify(overviewProxyService).addCallback(listenerCaptor.capture())
-        return listenerCaptor.firstValue
+        listenerCaptor.firstValue.updateContextualEduStats(isTrackpadGesture, gestureType)
     }
 
+    private fun getTargetDevice(gestureType: GestureType) =
+        when (gestureType) {
+            ALL_APPS -> DeviceType.KEYBOARD
+            else -> DeviceType.TOUCHPAD
+        }
+
     companion object {
         private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
index eeccbdf..79556ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.qs.tiles
 
 import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf
 import android.service.quicksettings.Tile
@@ -24,18 +26,26 @@
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
+import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.flags.setFlagValue
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.flags.QSComposeFragment
+import com.android.systemui.qs.flags.QsDetailedView
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
 import com.android.systemui.qs.tiles.dialog.WifiStateWorker
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.connectivity.AccessPointController
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
@@ -256,6 +266,41 @@
         verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true)
     }
 
+    @Test
+    @DisableFlags(QsDetailedView.FLAG_NAME)
+    fun click_withQsDetailedViewDisabled() {
+        underTest.click(null)
+        looper.processAllMessages()
+
+        verify(dialogManager, times(1)).create(
+            aboveStatusBar = true,
+            accessPointController.canConfigMobileData(),
+            accessPointController.canConfigWifi(),
+            null,
+        )
+    }
+
+    @Test
+    @EnableFlags(
+        value = [
+            QsDetailedView.FLAG_NAME,
+            FLAG_SCENE_CONTAINER,
+            KeyguardWmStateRefactor.FLAG_NAME,
+            NotificationThrottleHun.FLAG_NAME,
+            DualShade.FLAG_NAME]
+    )
+    fun click_withQsDetailedViewEnabled() {
+        underTest.click(null)
+        looper.processAllMessages()
+
+        verify(dialogManager, times(0)).create(
+            aboveStatusBar = true,
+            accessPointController.canConfigMobileData(),
+            accessPointController.canConfigWifi(),
+            null,
+        )
+    }
+
     companion object {
         const val WIFI_SSID = "test ssid"
         val ACTIVE_WIFI =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 20474c8..deaf579 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -526,7 +526,7 @@
         mLockscreenUserManager.mLastLockTime
                 .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
         // Device is not currently locked
-        when(mKeyguardManager.isDeviceLocked()).thenReturn(false);
+        mLockscreenUserManager.mLocked.set(false);
 
         // Sensitive Content notifications are always redacted
         assertEquals(REDACTION_TYPE_NONE,
@@ -540,7 +540,7 @@
         mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
                 mCurrentUser.id);
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+        mLockscreenUserManager.mLocked.set(true);
         // Device was locked after this notification arrived
         mLockscreenUserManager.mLastLockTime
                 .set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1));
@@ -560,7 +560,7 @@
         // Device has been locked for 1 second before the notification came in, which is too short
         mLockscreenUserManager.mLastLockTime
                 .set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1));
-        when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+        mLockscreenUserManager.mLocked.set(true);
 
         // Sensitive Content notifications are always redacted
         assertEquals(REDACTION_TYPE_NONE,
@@ -577,7 +577,7 @@
         // Claim the device was last locked 1 day ago
         mLockscreenUserManager.mLastLockTime
                 .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
-        when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+        mLockscreenUserManager.mLocked.set(true);
 
         // Sensitive Content notifications are always redacted
         assertEquals(REDACTION_TYPE_NONE,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 2d7dc2e..0a05649 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.util.WallpaperController
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
+import com.android.wm.shell.appzoomout.AppZoomOut
 import com.google.common.truth.Truth.assertThat
 import java.util.function.Consumer
 import org.junit.Before
@@ -65,6 +66,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
+import java.util.Optional
 
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
@@ -82,6 +84,7 @@
     @Mock private lateinit var wallpaperController: WallpaperController
     @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
     @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var appZoomOutOptional: Optional<AppZoomOut>
     @Mock private lateinit var root: View
     @Mock private lateinit var viewRootImpl: ViewRootImpl
     @Mock private lateinit var windowToken: IBinder
@@ -128,6 +131,7 @@
                 ResourcesSplitShadeStateController(),
                 windowRootViewBlurInteractor,
                 applicationScope,
+                appZoomOutOptional,
                 dumpManager,
                 configurationController,
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 72a91bc..14bbd38 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -279,7 +279,7 @@
                 notification);
         RemoteViews headerRemoteViews;
         if (lowPriority) {
-            headerRemoteViews = builder.makeLowPriorityContentView(true, false);
+            headerRemoteViews = builder.makeLowPriorityContentView(true);
         } else {
             headerRemoteViews = builder.makeNotificationGroupHeader();
         }
diff --git a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
index d93f7d3..81156d9 100644
--- a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
+++ b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
@@ -24,6 +24,7 @@
 import javax.lang.model.element.Element
 import javax.lang.model.element.ElementKind
 import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
 import javax.lang.model.element.PackageElement
 import javax.lang.model.element.TypeElement
 import javax.lang.model.type.TypeKind
@@ -183,11 +184,17 @@
                     // Method implementations
                     for (method in methods) {
                         val methodName = method.simpleName
+                        if (methods.any { methodName.startsWith("${it.simpleName}\$") }) {
+                            continue
+                        }
                         val returnTypeName = method.returnType.toString()
                         val callArgs = StringBuilder()
                         var isFirst = true
+                        val isStatic = method.modifiers.contains(Modifier.STATIC)
 
-                        line("@Override")
+                        if (!isStatic) {
+                            line("@Override")
+                        }
                         parenBlock("public $returnTypeName $methodName") {
                             // While copying the method signature for the proxy type, we also
                             // accumulate arguments for the nested callsite.
@@ -202,7 +209,8 @@
                         }
 
                         val isVoid = method.returnType.kind == TypeKind.VOID
-                        val nestedCall = "mInstance.$methodName($callArgs)"
+                        val methodContainer = if (isStatic) sourceName else "mInstance"
+                        val nestedCall = "$methodContainer.$methodName($callArgs)"
                         val callStatement =
                             when {
                                 isVoid -> "$nestedCall;"
diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml
new file mode 100644
index 0000000..f8c0fa0
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="scaleX"
+                    android:startOffset="1000"
+                    android:valueFrom="0.45561"
+                    android:valueTo="0.69699"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="scaleY"
+                    android:startOffset="1000"
+                    android:valueFrom="0.6288400000000001"
+                    android:valueTo="0.81618"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="scaleX"
+                    android:startOffset="1083"
+                    android:valueFrom="0.69699"
+                    android:valueTo="1.05905"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="scaleY"
+                    android:startOffset="1083"
+                    android:valueFrom="0.81618"
+                    android:valueTo="1.0972"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="90"
+                    android:valueTo="135"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="rotation"
+                    android:startOffset="500"
+                    android:valueFrom="135"
+                    android:valueTo="180"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="scaleX"
+                    android:startOffset="1000"
+                    android:valueFrom="0.0434"
+                    android:valueTo="0.05063"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="scaleY"
+                    android:startOffset="1000"
+                    android:valueFrom="0.0434"
+                    android:valueTo="0.042350000000000006"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="scaleX"
+                    android:startOffset="1083"
+                    android:valueFrom="0.05063"
+                    android:valueTo="0.06146"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="scaleY"
+                    android:startOffset="1083"
+                    android:valueFrom="0.042350000000000006"
+                    android:valueTo="0.040780000000000004"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1017"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="88dp"
+            android:height="56dp"
+            android:viewportHeight="56"
+            android:viewportWidth="88">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:pivotX="0.493"
+                    android:pivotY="0.124"
+                    android:scaleX="1.05905"
+                    android:scaleY="1.0972"
+                    android:translateX="43.528999999999996"
+                    android:translateY="27.898">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#3d90ff"
+                        android:fillType="nonZero"
+                        android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:rotation="0"
+                    android:scaleX="0.06146"
+                    android:scaleY="0.040780000000000004"
+                    android:translateX="44"
+                    android:translateY="28">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#3d90ff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-0.65 -437.37 C-0.65,-437.37 8.33,-437.66 8.33,-437.66 C8.33,-437.66 17.31,-437.95 17.31,-437.95 C17.31,-437.95 26.25,-438.78 26.25,-438.78 C26.25,-438.78 35.16,-439.95 35.16,-439.95 C35.16,-439.95 44.07,-441.11 44.07,-441.11 C44.07,-441.11 52.85,-443 52.85,-443 C52.85,-443 61.6,-445.03 61.6,-445.03 C61.6,-445.03 70.35,-447.09 70.35,-447.09 C70.35,-447.09 78.91,-449.83 78.91,-449.83 C78.91,-449.83 87.43,-452.67 87.43,-452.67 C87.43,-452.67 95.79,-455.97 95.79,-455.97 C95.79,-455.97 104.11,-459.35 104.11,-459.35 C104.11,-459.35 112.36,-462.93 112.36,-462.93 C112.36,-462.93 120.6,-466.51 120.6,-466.51 C120.6,-466.51 128.84,-470.09 128.84,-470.09 C128.84,-470.09 137.09,-473.67 137.09,-473.67 C137.09,-473.67 145.49,-476.84 145.49,-476.84 C145.49,-476.84 153.9,-480.01 153.9,-480.01 C153.9,-480.01 162.31,-483.18 162.31,-483.18 C162.31,-483.18 170.98,-485.54 170.98,-485.54 C170.98,-485.54 179.66,-487.85 179.66,-487.85 C179.66,-487.85 188.35,-490.15 188.35,-490.15 C188.35,-490.15 197.22,-491.58 197.22,-491.58 C197.22,-491.58 206.09,-493.01 206.09,-493.01 C206.09,-493.01 214.98,-494.28 214.98,-494.28 C214.98,-494.28 223.95,-494.81 223.95,-494.81 C223.95,-494.81 232.93,-495.33 232.93,-495.33 C232.93,-495.33 241.9,-495.5 241.9,-495.5 C241.9,-495.5 250.88,-495.13 250.88,-495.13 C250.88,-495.13 259.86,-494.75 259.86,-494.75 C259.86,-494.75 268.78,-493.78 268.78,-493.78 C268.78,-493.78 277.68,-492.52 277.68,-492.52 C277.68,-492.52 286.57,-491.26 286.57,-491.26 C286.57,-491.26 295.31,-489.16 295.31,-489.16 C295.31,-489.16 304.04,-487.04 304.04,-487.04 C304.04,-487.04 312.7,-484.65 312.7,-484.65 C312.7,-484.65 321.19,-481.72 321.19,-481.72 C321.19,-481.72 329.68,-478.78 329.68,-478.78 C329.68,-478.78 337.96,-475.31 337.96,-475.31 C337.96,-475.31 346.14,-471.59 346.14,-471.59 C346.14,-471.59 354.3,-467.82 354.3,-467.82 C354.3,-467.82 362.11,-463.38 362.11,-463.38 C362.11,-463.38 369.92,-458.93 369.92,-458.93 C369.92,-458.93 377.53,-454.17 377.53,-454.17 C377.53,-454.17 384.91,-449.04 384.91,-449.04 C384.91,-449.04 392.29,-443.91 392.29,-443.91 C392.29,-443.91 399.26,-438.24 399.26,-438.24 C399.26,-438.24 406.15,-432.48 406.15,-432.48 C406.15,-432.48 412.92,-426.57 412.92,-426.57 C412.92,-426.57 419.27,-420.22 419.27,-420.22 C419.27,-420.22 425.62,-413.87 425.62,-413.87 C425.62,-413.87 431.61,-407.18 431.61,-407.18 C431.61,-407.18 437.38,-400.29 437.38,-400.29 C437.38,-400.29 443.14,-393.39 443.14,-393.39 C443.14,-393.39 448.27,-386.01 448.27,-386.01 C448.27,-386.01 453.4,-378.64 453.4,-378.64 C453.4,-378.64 458.26,-371.09 458.26,-371.09 C458.26,-371.09 462.71,-363.28 462.71,-363.28 C462.71,-363.28 467.16,-355.47 467.16,-355.47 C467.16,-355.47 471.03,-347.37 471.03,-347.37 C471.03,-347.37 474.75,-339.19 474.75,-339.19 C474.75,-339.19 478.34,-330.95 478.34,-330.95 C478.34,-330.95 481.28,-322.46 481.28,-322.46 C481.28,-322.46 484.21,-313.97 484.21,-313.97 C484.21,-313.97 486.72,-305.35 486.72,-305.35 C486.72,-305.35 488.84,-296.62 488.84,-296.62 C488.84,-296.62 490.96,-287.88 490.96,-287.88 C490.96,-287.88 492.33,-279.01 492.33,-279.01 C492.33,-279.01 493.59,-270.11 493.59,-270.11 C493.59,-270.11 494.69,-261.2 494.69,-261.2 C494.69,-261.2 495.07,-252.22 495.07,-252.22 C495.07,-252.22 495.44,-243.24 495.44,-243.24 C495.44,-243.24 495.41,-234.27 495.41,-234.27 C495.41,-234.27 494.88,-225.29 494.88,-225.29 C494.88,-225.29 494.35,-216.32 494.35,-216.32 C494.35,-216.32 493.22,-207.42 493.22,-207.42 C493.22,-207.42 491.79,-198.55 491.79,-198.55 C491.79,-198.55 490.36,-189.68 490.36,-189.68 C490.36,-189.68 488.19,-180.96 488.19,-180.96 C488.19,-180.96 485.88,-172.28 485.88,-172.28 C485.88,-172.28 483.56,-163.6 483.56,-163.6 C483.56,-163.6 480.48,-155.16 480.48,-155.16 C480.48,-155.16 477.31,-146.75 477.31,-146.75 C477.31,-146.75 474.14,-138.34 474.14,-138.34 C474.14,-138.34 470.62,-130.07 470.62,-130.07 C470.62,-130.07 467.04,-121.83 467.04,-121.83 C467.04,-121.83 463.46,-113.59 463.46,-113.59 C463.46,-113.59 459.88,-105.35 459.88,-105.35 C459.88,-105.35 456.54,-97.01 456.54,-97.01 C456.54,-97.01 453.37,-88.6 453.37,-88.6 C453.37,-88.6 450.21,-80.19 450.21,-80.19 C450.21,-80.19 447.68,-71.57 447.68,-71.57 C447.68,-71.57 445.36,-62.89 445.36,-62.89 C445.36,-62.89 443.04,-54.21 443.04,-54.21 C443.04,-54.21 441.54,-45.35 441.54,-45.35 C441.54,-45.35 440.09,-36.48 440.09,-36.48 C440.09,-36.48 438.78,-27.6 438.78,-27.6 C438.78,-27.6 438.19,-18.63 438.19,-18.63 C438.19,-18.63 437.61,-9.66 437.61,-9.66 C437.61,-9.66 437.36,-0.69 437.36,-0.69 C437.36,-0.69 437.65,8.29 437.65,8.29 C437.65,8.29 437.95,17.27 437.95,17.27 C437.95,17.27 438.77,26.21 438.77,26.21 C438.77,26.21 439.94,35.12 439.94,35.12 C439.94,35.12 441.11,44.03 441.11,44.03 C441.11,44.03 442.99,52.81 442.99,52.81 C442.99,52.81 445.02,61.57 445.02,61.57 C445.02,61.57 447.07,70.31 447.07,70.31 C447.07,70.31 449.82,78.87 449.82,78.87 C449.82,78.87 452.65,87.4 452.65,87.4 C452.65,87.4 455.96,95.75 455.96,95.75 C455.96,95.75 459.33,104.08 459.33,104.08 C459.33,104.08 462.91,112.32 462.91,112.32 C462.91,112.32 466.49,120.57 466.49,120.57 C466.49,120.57 470.07,128.81 470.07,128.81 C470.07,128.81 473.65,137.05 473.65,137.05 C473.65,137.05 476.82,145.46 476.82,145.46 C476.82,145.46 479.99,153.87 479.99,153.87 C479.99,153.87 483.17,162.28 483.17,162.28 C483.17,162.28 485.52,170.94 485.52,170.94 C485.52,170.94 487.84,179.63 487.84,179.63 C487.84,179.63 490.14,188.31 490.14,188.31 C490.14,188.31 491.57,197.18 491.57,197.18 C491.57,197.18 493,206.06 493,206.06 C493,206.06 494.27,214.95 494.27,214.95 C494.27,214.95 494.8,223.92 494.8,223.92 C494.8,223.92 495.33,232.89 495.33,232.89 C495.33,232.89 495.5,241.86 495.5,241.86 C495.5,241.86 495.12,250.84 495.12,250.84 C495.12,250.84 494.75,259.82 494.75,259.82 C494.75,259.82 493.78,268.74 493.78,268.74 C493.78,268.74 492.52,277.64 492.52,277.64 C492.52,277.64 491.27,286.54 491.27,286.54 C491.27,286.54 489.16,295.27 489.16,295.27 C489.16,295.27 487.05,304.01 487.05,304.01 C487.05,304.01 484.66,312.66 484.66,312.66 C484.66,312.66 481.73,321.16 481.73,321.16 C481.73,321.16 478.79,329.65 478.79,329.65 C478.79,329.65 475.32,337.93 475.32,337.93 C475.32,337.93 471.6,346.11 471.6,346.11 C471.6,346.11 467.84,354.27 467.84,354.27 C467.84,354.27 463.39,362.08 463.39,362.08 C463.39,362.08 458.94,369.89 458.94,369.89 C458.94,369.89 454.19,377.5 454.19,377.5 C454.19,377.5 449.06,384.88 449.06,384.88 C449.06,384.88 443.93,392.26 443.93,392.26 C443.93,392.26 438.26,399.23 438.26,399.23 C438.26,399.23 432.5,406.12 432.5,406.12 C432.5,406.12 426.6,412.89 426.6,412.89 C426.6,412.89 420.24,419.24 420.24,419.24 C420.24,419.24 413.89,425.6 413.89,425.6 C413.89,425.6 407.2,431.59 407.2,431.59 C407.2,431.59 400.31,437.36 400.31,437.36 C400.31,437.36 393.42,443.12 393.42,443.12 C393.42,443.12 386.04,448.25 386.04,448.25 C386.04,448.25 378.66,453.38 378.66,453.38 C378.66,453.38 371.11,458.24 371.11,458.24 C371.11,458.24 363.31,462.69 363.31,462.69 C363.31,462.69 355.5,467.14 355.5,467.14 C355.5,467.14 347.4,471.02 347.4,471.02 C347.4,471.02 339.22,474.73 339.22,474.73 C339.22,474.73 330.99,478.33 330.99,478.33 C330.99,478.33 322.49,481.27 322.49,481.27 C322.49,481.27 314,484.2 314,484.2 C314,484.2 305.38,486.71 305.38,486.71 C305.38,486.71 296.65,488.83 296.65,488.83 C296.65,488.83 287.91,490.95 287.91,490.95 C287.91,490.95 279.04,492.33 279.04,492.33 C279.04,492.33 270.14,493.59 270.14,493.59 C270.14,493.59 261.23,494.69 261.23,494.69 C261.23,494.69 252.25,495.07 252.25,495.07 C252.25,495.07 243.28,495.44 243.28,495.44 C243.28,495.44 234.3,495.41 234.3,495.41 C234.3,495.41 225.33,494.88 225.33,494.88 C225.33,494.88 216.36,494.35 216.36,494.35 C216.36,494.35 207.45,493.23 207.45,493.23 C207.45,493.23 198.58,491.8 198.58,491.8 C198.58,491.8 189.71,490.37 189.71,490.37 C189.71,490.37 180.99,488.21 180.99,488.21 C180.99,488.21 172.31,485.89 172.31,485.89 C172.31,485.89 163.63,483.57 163.63,483.57 C163.63,483.57 155.19,480.5 155.19,480.5 C155.19,480.5 146.78,477.32 146.78,477.32 C146.78,477.32 138.37,474.15 138.37,474.15 C138.37,474.15 130.11,470.63 130.11,470.63 C130.11,470.63 121.86,467.06 121.86,467.06 C121.86,467.06 113.62,463.48 113.62,463.48 C113.62,463.48 105.38,459.9 105.38,459.9 C105.38,459.9 97.04,456.56 97.04,456.56 C97.04,456.56 88.63,453.39 88.63,453.39 C88.63,453.39 80.22,450.22 80.22,450.22 C80.22,450.22 71.6,447.7 71.6,447.7 C71.6,447.7 62.92,445.37 62.92,445.37 C62.92,445.37 54.24,443.05 54.24,443.05 C54.24,443.05 45.38,441.55 45.38,441.55 C45.38,441.55 36.52,440.1 36.52,440.1 C36.52,440.1 27.63,438.78 27.63,438.78 C27.63,438.78 18.66,438.2 18.66,438.2 C18.66,438.2 9.7,437.61 9.7,437.61 C9.7,437.61 0.72,437.36 0.72,437.36 C0.72,437.36 -8.26,437.65 -8.26,437.65 C-8.26,437.65 -17.24,437.95 -17.24,437.95 C-17.24,437.95 -26.18,438.77 -26.18,438.77 C-26.18,438.77 -35.09,439.94 -35.09,439.94 C-35.09,439.94 -44,441.1 -44,441.1 C-44,441.1 -52.78,442.98 -52.78,442.98 C-52.78,442.98 -61.53,445.02 -61.53,445.02 C-61.53,445.02 -70.28,447.07 -70.28,447.07 C-70.28,447.07 -78.84,449.81 -78.84,449.81 C-78.84,449.81 -87.37,452.64 -87.37,452.64 C-87.37,452.64 -95.72,455.95 -95.72,455.95 C-95.72,455.95 -104.05,459.32 -104.05,459.32 C-104.05,459.32 -112.29,462.9 -112.29,462.9 C-112.29,462.9 -120.53,466.48 -120.53,466.48 C-120.53,466.48 -128.78,470.06 -128.78,470.06 C-128.78,470.06 -137.02,473.63 -137.02,473.63 C-137.02,473.63 -145.43,476.81 -145.43,476.81 C-145.43,476.81 -153.84,479.98 -153.84,479.98 C-153.84,479.98 -162.24,483.15 -162.24,483.15 C-162.24,483.15 -170.91,485.52 -170.91,485.52 C-170.91,485.52 -179.59,487.83 -179.59,487.83 C-179.59,487.83 -188.28,490.13 -188.28,490.13 C-188.28,490.13 -197.15,491.56 -197.15,491.56 C-197.15,491.56 -206.02,492.99 -206.02,492.99 C-206.02,492.99 -214.91,494.27 -214.91,494.27 C-214.91,494.27 -223.88,494.8 -223.88,494.8 C-223.88,494.8 -232.85,495.33 -232.85,495.33 C-232.85,495.33 -241.83,495.5 -241.83,495.5 C-241.83,495.5 -250.81,495.13 -250.81,495.13 C-250.81,495.13 -259.79,494.75 -259.79,494.75 C-259.79,494.75 -268.71,493.79 -268.71,493.79 C-268.71,493.79 -277.61,492.53 -277.61,492.53 C-277.61,492.53 -286.51,491.27 -286.51,491.27 C-286.51,491.27 -295.24,489.17 -295.24,489.17 C-295.24,489.17 -303.98,487.06 -303.98,487.06 C-303.98,487.06 -312.63,484.67 -312.63,484.67 C-312.63,484.67 -321.12,481.74 -321.12,481.74 C-321.12,481.74 -329.62,478.8 -329.62,478.8 C-329.62,478.8 -337.9,475.33 -337.9,475.33 C-337.9,475.33 -346.08,471.62 -346.08,471.62 C-346.08,471.62 -354.24,467.85 -354.24,467.85 C-354.24,467.85 -362.05,463.41 -362.05,463.41 C-362.05,463.41 -369.86,458.96 -369.86,458.96 C-369.86,458.96 -377.47,454.21 -377.47,454.21 C-377.47,454.21 -384.85,449.08 -384.85,449.08 C-384.85,449.08 -392.23,443.95 -392.23,443.95 C-392.23,443.95 -399.2,438.29 -399.2,438.29 C-399.2,438.29 -406.09,432.52 -406.09,432.52 C-406.09,432.52 -412.86,426.62 -412.86,426.62 C-412.86,426.62 -419.22,420.27 -419.22,420.27 C-419.22,420.27 -425.57,413.91 -425.57,413.91 C-425.57,413.91 -431.57,407.23 -431.57,407.23 C-431.57,407.23 -437.33,400.34 -437.33,400.34 C-437.33,400.34 -443.1,393.44 -443.1,393.44 C-443.1,393.44 -448.23,386.07 -448.23,386.07 C-448.23,386.07 -453.36,378.69 -453.36,378.69 C-453.36,378.69 -458.23,371.15 -458.23,371.15 C-458.23,371.15 -462.67,363.33 -462.67,363.33 C-462.67,363.33 -467.12,355.53 -467.12,355.53 C-467.12,355.53 -471,347.43 -471,347.43 C-471,347.43 -474.72,339.25 -474.72,339.25 C-474.72,339.25 -478.32,331.02 -478.32,331.02 C-478.32,331.02 -481.25,322.52 -481.25,322.52 C-481.25,322.52 -484.19,314.03 -484.19,314.03 C-484.19,314.03 -486.71,305.42 -486.71,305.42 C-486.71,305.42 -488.82,296.68 -488.82,296.68 C-488.82,296.68 -490.94,287.95 -490.94,287.95 C-490.94,287.95 -492.32,279.07 -492.32,279.07 C-492.32,279.07 -493.58,270.18 -493.58,270.18 C-493.58,270.18 -494.69,261.27 -494.69,261.27 C-494.69,261.27 -495.07,252.29 -495.07,252.29 C-495.07,252.29 -495.44,243.31 -495.44,243.31 C-495.44,243.31 -495.42,234.33 -495.42,234.33 C-495.42,234.33 -494.89,225.36 -494.89,225.36 C-494.89,225.36 -494.36,216.39 -494.36,216.39 C-494.36,216.39 -493.23,207.49 -493.23,207.49 C-493.23,207.49 -491.8,198.61 -491.8,198.61 C-491.8,198.61 -490.37,189.74 -490.37,189.74 C-490.37,189.74 -488.22,181.02 -488.22,181.02 C-488.22,181.02 -485.9,172.34 -485.9,172.34 C-485.9,172.34 -483.58,163.66 -483.58,163.66 C-483.58,163.66 -480.51,155.22 -480.51,155.22 C-480.51,155.22 -477.34,146.81 -477.34,146.81 C-477.34,146.81 -474.17,138.41 -474.17,138.41 C-474.17,138.41 -470.65,130.14 -470.65,130.14 C-470.65,130.14 -467.07,121.9 -467.07,121.9 C-467.07,121.9 -463.49,113.65 -463.49,113.65 C-463.49,113.65 -459.91,105.41 -459.91,105.41 C-459.91,105.41 -456.57,97.07 -456.57,97.07 C-456.57,97.07 -453.4,88.66 -453.4,88.66 C-453.4,88.66 -450.23,80.25 -450.23,80.25 C-450.23,80.25 -447.7,71.64 -447.7,71.64 C-447.7,71.64 -445.38,62.96 -445.38,62.96 C-445.38,62.96 -443.06,54.28 -443.06,54.28 C-443.06,54.28 -441.56,45.42 -441.56,45.42 C-441.56,45.42 -440.1,36.55 -440.1,36.55 C-440.1,36.55 -438.78,27.67 -438.78,27.67 C-438.78,27.67 -438.2,18.7 -438.2,18.7 C-438.2,18.7 -437.62,9.73 -437.62,9.73 C-437.62,9.73 -437.36,0.76 -437.36,0.76 C-437.36,0.76 -437.66,-8.22 -437.66,-8.22 C-437.66,-8.22 -437.95,-17.2 -437.95,-17.2 C-437.95,-17.2 -438.77,-26.14 -438.77,-26.14 C-438.77,-26.14 -439.93,-35.05 -439.93,-35.05 C-439.93,-35.05 -441.1,-43.96 -441.1,-43.96 C-441.1,-43.96 -442.98,-52.75 -442.98,-52.75 C-442.98,-52.75 -445.01,-61.5 -445.01,-61.5 C-445.01,-61.5 -447.06,-70.25 -447.06,-70.25 C-447.06,-70.25 -449.8,-78.81 -449.8,-78.81 C-449.8,-78.81 -452.63,-87.33 -452.63,-87.33 C-452.63,-87.33 -455.94,-95.69 -455.94,-95.69 C-455.94,-95.69 -459.31,-104.02 -459.31,-104.02 C-459.31,-104.02 -462.89,-112.26 -462.89,-112.26 C-462.89,-112.26 -466.47,-120.5 -466.47,-120.5 C-466.47,-120.5 -470.05,-128.74 -470.05,-128.74 C-470.05,-128.74 -473.68,-137.12 -473.68,-137.12 C-473.68,-137.12 -476.85,-145.53 -476.85,-145.53 C-476.85,-145.53 -480.03,-153.94 -480.03,-153.94 C-480.03,-153.94 -483.2,-162.34 -483.2,-162.34 C-483.2,-162.34 -485.55,-171.02 -485.55,-171.02 C-485.55,-171.02 -487.86,-179.7 -487.86,-179.7 C-487.86,-179.7 -490.15,-188.39 -490.15,-188.39 C-490.15,-188.39 -491.58,-197.26 -491.58,-197.26 C-491.58,-197.26 -493.01,-206.13 -493.01,-206.13 C-493.01,-206.13 -494.28,-215.02 -494.28,-215.02 C-494.28,-215.02 -494.81,-223.99 -494.81,-223.99 C-494.81,-223.99 -495.33,-232.96 -495.33,-232.96 C-495.33,-232.96 -495.5,-241.94 -495.5,-241.94 C-495.5,-241.94 -495.12,-250.92 -495.12,-250.92 C-495.12,-250.92 -494.75,-259.9 -494.75,-259.9 C-494.75,-259.9 -493.78,-268.82 -493.78,-268.82 C-493.78,-268.82 -492.52,-277.72 -492.52,-277.72 C-492.52,-277.72 -491.26,-286.61 -491.26,-286.61 C-491.26,-286.61 -489.15,-295.35 -489.15,-295.35 C-489.15,-295.35 -487.03,-304.08 -487.03,-304.08 C-487.03,-304.08 -484.64,-312.73 -484.64,-312.73 C-484.64,-312.73 -481.7,-321.23 -481.7,-321.23 C-481.7,-321.23 -478.77,-329.72 -478.77,-329.72 C-478.77,-329.72 -475.29,-338 -475.29,-338 C-475.29,-338 -471.57,-346.18 -471.57,-346.18 C-471.57,-346.18 -467.8,-354.33 -467.8,-354.33 C-467.8,-354.33 -463.36,-362.14 -463.36,-362.14 C-463.36,-362.14 -458.91,-369.95 -458.91,-369.95 C-458.91,-369.95 -454.15,-377.56 -454.15,-377.56 C-454.15,-377.56 -449.02,-384.94 -449.02,-384.94 C-449.02,-384.94 -443.88,-392.32 -443.88,-392.32 C-443.88,-392.32 -438.22,-399.28 -438.22,-399.28 C-438.22,-399.28 -432.45,-406.18 -432.45,-406.18 C-432.45,-406.18 -426.55,-412.94 -426.55,-412.94 C-426.55,-412.94 -420.19,-419.3 -420.19,-419.3 C-420.19,-419.3 -413.84,-425.65 -413.84,-425.65 C-413.84,-425.65 -407.15,-431.64 -407.15,-431.64 C-407.15,-431.64 -400.26,-437.41 -400.26,-437.41 C-400.26,-437.41 -393.36,-443.16 -393.36,-443.16 C-393.36,-443.16 -385.98,-448.29 -385.98,-448.29 C-385.98,-448.29 -378.6,-453.43 -378.6,-453.43 C-378.6,-453.43 -371.05,-458.28 -371.05,-458.28 C-371.05,-458.28 -363.24,-462.73 -363.24,-462.73 C-363.24,-462.73 -355.43,-467.18 -355.43,-467.18 C-355.43,-467.18 -347.33,-471.05 -347.33,-471.05 C-347.33,-471.05 -339.15,-474.76 -339.15,-474.76 C-339.15,-474.76 -330.92,-478.35 -330.92,-478.35 C-330.92,-478.35 -322.42,-481.29 -322.42,-481.29 C-322.42,-481.29 -313.93,-484.23 -313.93,-484.23 C-313.93,-484.23 -305.31,-486.73 -305.31,-486.73 C-305.31,-486.73 -296.58,-488.85 -296.58,-488.85 C-296.58,-488.85 -287.85,-490.97 -287.85,-490.97 C-287.85,-490.97 -278.97,-492.34 -278.97,-492.34 C-278.97,-492.34 -270.07,-493.6 -270.07,-493.6 C-270.07,-493.6 -261.16,-494.7 -261.16,-494.7 C-261.16,-494.7 -252.18,-495.07 -252.18,-495.07 C-252.18,-495.07 -243.2,-495.44 -243.2,-495.44 C-243.2,-495.44 -234.23,-495.41 -234.23,-495.41 C-234.23,-495.41 -225.26,-494.88 -225.26,-494.88 C-225.26,-494.88 -216.29,-494.35 -216.29,-494.35 C-216.29,-494.35 -207.38,-493.22 -207.38,-493.22 C-207.38,-493.22 -198.51,-491.79 -198.51,-491.79 C-198.51,-491.79 -189.64,-490.36 -189.64,-490.36 C-189.64,-490.36 -180.92,-488.19 -180.92,-488.19 C-180.92,-488.19 -172.24,-485.87 -172.24,-485.87 C-172.24,-485.87 -163.56,-483.56 -163.56,-483.56 C-163.56,-483.56 -155.12,-480.47 -155.12,-480.47 C-155.12,-480.47 -146.72,-477.3 -146.72,-477.3 C-146.72,-477.3 -138.31,-474.13 -138.31,-474.13 C-138.31,-474.13 -130.04,-470.61 -130.04,-470.61 C-130.04,-470.61 -121.8,-467.03 -121.8,-467.03 C-121.8,-467.03 -113.55,-463.45 -113.55,-463.45 C-113.55,-463.45 -105.31,-459.87 -105.31,-459.87 C-105.31,-459.87 -96.97,-456.53 -96.97,-456.53 C-96.97,-456.53 -88.56,-453.37 -88.56,-453.37 C-88.56,-453.37 -80.15,-450.2 -80.15,-450.2 C-80.15,-450.2 -71.53,-447.68 -71.53,-447.68 C-71.53,-447.68 -62.85,-445.36 -62.85,-445.36 C-62.85,-445.36 -54.17,-443.04 -54.17,-443.04 C-54.17,-443.04 -45.31,-441.54 -45.31,-441.54 C-45.31,-441.54 -36.44,-440.09 -36.44,-440.09 C-36.44,-440.09 -27.56,-438.78 -27.56,-438.78 C-27.56,-438.78 -18.59,-438.19 -18.59,-438.19 C-18.59,-438.19 -9.62,-437.61 -9.62,-437.61 C-9.62,-437.61 -0.65,-437.37 -0.65,-437.37c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button.xml b/packages/SystemUI/res/drawable/ic_media_pause_button.xml
new file mode 100644
index 0000000..6ae89f9
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_pause_button.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+                    android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+                    android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="56"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="15.485"
+                    android:valueTo="12.321"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="278"
+                    android:propertyName="translateX"
+                    android:startOffset="56"
+                    android:valueFrom="12.321"
+                    android:valueTo="7.576"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="517"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:pivotX="-12.031"
+                    android:scaleX="0.33299999999999996"
+                    android:scaleY="0.33299999999999996"
+                    android:translateX="19.524"
+                    android:translateY="12.084">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_T_1"
+                    android:scaleX="0.33299999999999996"
+                    android:scaleY="0.33299999999999996"
+                    android:translateX="15.485"
+                    android:translateY="12.084">
+                    <group
+                        android:name="_R_G_L_0_G"
+                        android:translateX="12.031">
+                        <path
+                            android:name="_R_G_L_0_G_D_0_P_0"
+                            android:fillAlpha="1"
+                            android:fillColor="#ffffff"
+                            android:fillType="nonZero"
+                            android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " />
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml
new file mode 100644
index 0000000..571f69d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="88dp"
+            android:height="56dp"
+            android:viewportHeight="56"
+            android:viewportWidth="88">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:pivotX="0.493"
+                    android:pivotY="0.124"
+                    android:scaleX="1.05905"
+                    android:scaleY="1.0972"
+                    android:translateX="43.528999999999996"
+                    android:translateY="27.898">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#3d90ff"
+                        android:fillType="nonZero"
+                        android:pathData=" M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="133"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+                    android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="367"
+                    android:propertyName="pathData"
+                    android:startOffset="133"
+                    android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+                    android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="scaleX"
+                    android:startOffset="0"
+                    android:valueFrom="1.05905"
+                    android:valueTo="1.17758"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="scaleY"
+                    android:startOffset="0"
+                    android:valueFrom="1.0972"
+                    android:valueTo="1.22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="scaleX"
+                    android:startOffset="167"
+                    android:valueFrom="1.17758"
+                    android:valueTo="1.05905"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="scaleY"
+                    android:startOffset="167"
+                    android:valueFrom="1.22"
+                    android:valueTo="1.0972"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="517"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_play_button.xml b/packages/SystemUI/res/drawable/ic_media_play_button.xml
new file mode 100644
index 0000000..f646902
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_play_button.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+                    android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+                    android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="7.576"
+                    android:valueTo="15.485"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.583,0 0.089,0.874 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="517"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:pivotX="-12.031"
+                    android:scaleX="0.33299999999999996"
+                    android:scaleY="0.33299999999999996"
+                    android:translateX="19.524"
+                    android:translateY="12.084">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_T_1"
+                    android:scaleX="0.33299999999999996"
+                    android:scaleY="0.33299999999999996"
+                    android:translateX="7.576"
+                    android:translateY="12.084">
+                    <group
+                        android:name="_R_G_L_0_G"
+                        android:translateX="12.031">
+                        <path
+                            android:name="_R_G_L_0_G_D_0_P_0"
+                            android:fillAlpha="1"
+                            android:fillColor="#ffffff"
+                            android:fillType="nonZero"
+                            android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " />
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_play_button_container.xml b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml
new file mode 100644
index 0000000..aa4e09fa
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:height="56dp"
+            android:width="88dp"
+            android:viewportHeight="56"
+            android:viewportWidth="88">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="43.528999999999996"
+                    android:translateY="27.898"
+                    android:pivotX="0.493"
+                    android:pivotY="0.124"
+                    android:scaleX="1.05905"
+                    android:scaleY="1.0972">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillColor="#3d90ff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "/>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="167"
+                    android:startOffset="0"
+                    android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+                    android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="333"
+                    android:startOffset="167"
+                    android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+                    android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="167"
+                    android:startOffset="0"
+                    android:valueFrom="1.05905"
+                    android:valueTo="1.17758"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="167"
+                    android:startOffset="0"
+                    android:valueFrom="1.0972"
+                    android:valueTo="1.22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="333"
+                    android:startOffset="167"
+                    android:valueFrom="1.17758"
+                    android:valueTo="1.05905"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="333"
+                    android:startOffset="167"
+                    android:valueFrom="1.22"
+                    android:valueTo="1.0972"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="translateX"
+                    android:duration="517"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 1f78892..2ffa3d1 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1278,6 +1278,7 @@
     <dimen name="qs_center_guideline_padding">10dp</dimen>
     <dimen name="qs_media_action_spacing">4dp</dimen>
     <dimen name="qs_media_action_margin">12dp</dimen>
+    <dimen name="qs_media_action_play_pause_width">72dp</dimen>
     <dimen name="qs_seamless_height">24dp</dimen>
     <dimen name="qs_seamless_icon_size">12dp</dimen>
     <dimen name="qs_media_disabled_seekbar_height">1dp</dimen>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
index 51892aa..ff6bcdb 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
@@ -19,6 +19,7 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.RemoteAnimationTarget;
+import android.window.TransitionInfo;
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -30,7 +31,7 @@
      */
     void onAnimationStart(RecentsAnimationControllerCompat controller,
             RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
-            Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras);
+            Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras, TransitionInfo info);
 
     /**
      * Called when the animation into Recents was canceled. This call is made on the binder thread.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index acfa086..c7ae02b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -142,33 +142,28 @@
 
     private boolean isKeyguardShowable(Display display) {
         if (display == null) {
-            if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display");
+            Log.i(TAG, "Cannot show Keyguard on null display");
             return false;
         }
         if (ShadeWindowGoesAround.isEnabled()) {
             int shadeDisplayId = mShadePositionRepositoryProvider.get().getDisplayId().getValue();
             if (display.getDisplayId() == shadeDisplayId) {
-                if (DEBUG) {
-                    Log.i(TAG,
-                            "Do not show KeyguardPresentation on the shade window display");
-                }
+                Log.i(TAG, "Do not show KeyguardPresentation on the shade window display");
                 return false;
             }
         } else {
             if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
-                if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
+                Log.i(TAG, "Do not show KeyguardPresentation on the default display");
                 return false;
             }
         }
         display.getDisplayInfo(mTmpDisplayInfo);
         if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) {
-            if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on a private display");
+            Log.i(TAG, "Do not show KeyguardPresentation on a private display");
             return false;
         }
         if ((mTmpDisplayInfo.flags & Display.FLAG_ALWAYS_UNLOCKED) != 0) {
-            if (DEBUG) {
-                Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display");
-            }
+            Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display");
             return false;
         }
 
@@ -176,14 +171,11 @@
                 mDeviceStateHelper.isConcurrentDisplayActive(display)
                         || mDeviceStateHelper.isRearDisplayOuterDefaultActive(display);
         if (mKeyguardStateController.isOccluded() && deviceStateOccludesKeyguard) {
-            if (DEBUG) {
-                // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the
-                // Keyguard state becomes "occluded". In this case, we should not show the
-                // KeyguardPresentation, since the activity is presenting content onto the
-                // non-default display.
-                Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear"
-                        + " display is active");
-            }
+            // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the Keyguard
+            // state becomes "occluded". In this case, we should not show the KeyguardPresentation,
+            // since the activity is presenting content onto the non-default display.
+            Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear"
+                    + " display is active");
             return false;
         }
 
@@ -197,7 +189,7 @@
      */
     private boolean showPresentation(Display display) {
         if (!isKeyguardShowable(display)) return false;
-        if (DEBUG) Log.i(TAG, "Keyguard enabled on display: " + display);
+        Log.i(TAG, "Keyguard enabled on display: " + display);
         final int displayId = display.getDisplayId();
         Presentation presentation = mPresentations.get(displayId);
         if (presentation == null) {
@@ -239,7 +231,7 @@
 
     public void show() {
         if (!mShowing) {
-            if (DEBUG) Log.v(TAG, "show");
+            Log.v(TAG, "show");
             if (mMediaRouter != null) {
                 mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY,
                         mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
@@ -253,7 +245,7 @@
 
     public void hide() {
         if (mShowing) {
-            if (DEBUG) Log.v(TAG, "hide");
+            Log.v(TAG, "hide");
             if (mMediaRouter != null) {
                 mMediaRouter.removeCallback(mMediaRouterCallback);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index f530522..5f79c8c 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -100,7 +100,8 @@
                     .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
                     .setRecentTasks(mWMComponent.getRecentTasks())
                     .setBackAnimation(mWMComponent.getBackAnimation())
-                    .setDesktopMode(mWMComponent.getDesktopMode());
+                    .setDesktopMode(mWMComponent.getDesktopMode())
+                    .setAppZoomOut(mWMComponent.getAppZoomOut());
 
             // Only initialize when not starting from tests since this currently initializes some
             // components that shouldn't be run in the test environment
@@ -121,7 +122,8 @@
                     .setStartingSurface(Optional.ofNullable(null))
                     .setRecentTasks(Optional.ofNullable(null))
                     .setBackAnimation(Optional.ofNullable(null))
-                    .setDesktopMode(Optional.ofNullable(null));
+                    .setDesktopMode(Optional.ofNullable(null))
+                    .setAppZoomOut(Optional.ofNullable(null));
         }
         mSysUIComponent = builder.build();
 
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
index 4dc2a13..0303048 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
@@ -104,6 +104,31 @@
         }
     }
 
+    override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) {
+        withContext(backgroundDispatcher) {
+            if (!audioSharingInteractor.audioSharingAvailable()) {
+                return@withContext deviceItemActionInteractorImpl.onActionIconClick(
+                    deviceItem,
+                    onIntent,
+                )
+            }
+
+            when (deviceItem.type) {
+                DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.CHECK_MARK_ACTION_BUTTON_CLICKED)
+                    audioSharingInteractor.stopAudioSharing()
+                }
+                DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.PLUS_ACTION_BUTTON_CLICKED)
+                    audioSharingInteractor.startAudioSharing()
+                }
+                else -> {
+                    deviceItemActionInteractorImpl.onActionIconClick(deviceItem, onIntent)
+                }
+            }
+        }
+    }
+
     private fun inSharingAndDeviceNoSource(
         inAudioSharing: Boolean,
         deviceItem: DeviceItem,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
index c4f26cd..116e76c 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
@@ -29,6 +29,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.flatMapLatest
@@ -54,6 +55,8 @@
 
     suspend fun startAudioSharing()
 
+    suspend fun stopAudioSharing()
+
     suspend fun audioSharingAvailable(): Boolean
 
     suspend fun qsDialogImprovementAvailable(): Boolean
@@ -61,7 +64,7 @@
 
 @SysUISingleton
 @OptIn(ExperimentalCoroutinesApi::class)
-class AudioSharingInteractorImpl
+open class AudioSharingInteractorImpl
 @Inject
 constructor(
     private val context: Context,
@@ -99,6 +102,9 @@
             if (audioSharingAvailable()) {
                 audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
                     isAudioSharingOn
+                        // Skip the default value, we only care about adding source for newly
+                        // started audio sharing session
+                        .drop(1)
                         .mapNotNull { audioSharingOn ->
                             if (audioSharingOn) {
                                 // onBroadcastMetadataChanged could emit multiple times during one
@@ -145,6 +151,13 @@
         audioSharingRepository.startAudioSharing()
     }
 
+    override suspend fun stopAudioSharing() {
+        if (!audioSharingAvailable()) {
+            return
+        }
+        audioSharingRepository.stopAudioSharing()
+    }
+
     // TODO(b/367965193): Move this after flags rollout
     override suspend fun audioSharingAvailable(): Boolean {
         return audioSharingRepository.audioSharingAvailable()
@@ -181,6 +194,8 @@
 
     override suspend fun startAudioSharing() {}
 
+    override suspend fun stopAudioSharing() {}
+
     override suspend fun audioSharingAvailable(): Boolean = false
 
     override suspend fun qsDialogImprovementAvailable(): Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
index b9b8d36..44f9769 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
@@ -45,6 +45,8 @@
     suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice)
 
     suspend fun startAudioSharing()
+
+    suspend fun stopAudioSharing()
 }
 
 @SysUISingleton
@@ -100,6 +102,15 @@
             leAudioBroadcastProfile?.startPrivateBroadcast()
         }
     }
+
+    override suspend fun stopAudioSharing() {
+        withContext(backgroundDispatcher) {
+            if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+                return@withContext
+            }
+            leAudioBroadcastProfile?.stopLatestBroadcast()
+        }
+    }
 }
 
 @SysUISingleton
@@ -117,4 +128,6 @@
     override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
 
     override suspend fun startAudioSharing() {}
+
+    override suspend fun stopAudioSharing() {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index b294dd1..56caddf 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -56,6 +56,13 @@
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.withContext
 
+data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) {
+    enum class Target {
+        ENTIRE_ROW,
+        ACTION_ICON,
+    }
+}
+
 /** Dialog for showing active, connected and saved bluetooth devices. */
 class BluetoothTileDialogDelegate
 @AssistedInject
@@ -80,7 +87,7 @@
     internal val bluetoothAutoOnToggle
         get() = mutableBluetoothAutoOnToggle.asStateFlow()
 
-    private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> =
+    private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> =
         MutableSharedFlow(extraBufferCapacity = 1)
     internal val deviceItemClick
         get() = mutableDeviceItemClick.asSharedFlow()
@@ -90,7 +97,7 @@
     internal val contentHeight
         get() = mutableContentHeight.asSharedFlow()
 
-    private val deviceItemAdapter: Adapter = Adapter(bluetoothTileDialogCallback)
+    private val deviceItemAdapter: Adapter = Adapter()
 
     private var lastUiUpdateMs: Long = -1
 
@@ -334,8 +341,7 @@
         }
     }
 
-    internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) :
-        RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+    internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
 
         private val diffUtilCallback =
             object : DiffUtil.ItemCallback<DeviceItem>() {
@@ -376,7 +382,7 @@
 
         override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
             val item = getItem(position)
-            holder.bind(item, onClickCallback)
+            holder.bind(item)
         }
 
         internal fun getItem(position: Int) = asyncListDiffer.currentList[position]
@@ -390,19 +396,18 @@
             private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
             private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
             private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
-            private val iconGear = view.requireViewById<ImageView>(R.id.gear_icon_image)
-            private val gearView = view.requireViewById<View>(R.id.gear_icon)
+            private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image)
+            private val actionIconView = view.requireViewById<View>(R.id.gear_icon)
             private val divider = view.requireViewById<View>(R.id.divider)
 
-            internal fun bind(
-                item: DeviceItem,
-                deviceItemOnClickCallback: BluetoothTileDialogCallback,
-            ) {
+            internal fun bind(item: DeviceItem) {
                 container.apply {
                     isEnabled = item.isEnabled
                     background = item.background?.let { context.getDrawable(it) }
                     setOnClickListener {
-                        mutableDeviceItemClick.tryEmit(item)
+                        mutableDeviceItemClick.tryEmit(
+                            DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW)
+                        )
                         uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
                     }
 
@@ -421,7 +426,8 @@
                         }
                     }
 
-                    iconGear.apply { drawable?.let { it.mutate()?.setTint(tintColor) } }
+                    actionIcon.setImageResource(item.actionIconRes)
+                    actionIcon.drawable?.setTint(tintColor)
 
                     divider.setBackgroundColor(tintColor)
 
@@ -454,8 +460,10 @@
                 nameView.text = item.deviceName
                 summaryView.text = item.connectionSummary
 
-                gearView.setOnClickListener {
-                    deviceItemOnClickCallback.onDeviceItemGearClicked(item, it)
+                actionIconView.setOnClickListener {
+                    mutableDeviceItemClick.tryEmit(
+                        DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON)
+                    )
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
index aad233f..7c66ec0 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
@@ -49,7 +49,7 @@
     LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED(1719),
     @Deprecated(
         "Use case no longer needed",
-        ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED")
+        ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED"),
     )
     @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked")
     LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720),
@@ -59,7 +59,11 @@
     @UiEvent(doc = "Clicked on switch active button on audio sharing dialog")
     AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890),
     @UiEvent(doc = "Clicked on share audio button on audio sharing dialog")
-    AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891);
+    AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891),
+    @UiEvent(doc = "Clicked on plus action button")
+    PLUS_ACTION_BUTTON_CLICKED(2061),
+    @UiEvent(doc = "Clicked on checkmark action button")
+    CHECK_MARK_ACTION_BUTTON_CLICKED(2062);
 
     override fun getId() = metricId
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index 497d8cf..9460e7c 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
 import com.android.systemui.dagger.SysUISingleton
@@ -227,8 +226,22 @@
                 // deviceItemClick is emitted when user clicked on a device item.
                 dialogDelegate.deviceItemClick
                     .onEach {
-                        deviceItemActionInteractor.onClick(it, dialog)
-                        logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type)
+                        when (it.target) {
+                            DeviceItemClick.Target.ENTIRE_ROW -> {
+                                deviceItemActionInteractor.onClick(it.deviceItem, dialog)
+                                logger.logDeviceClick(
+                                    it.deviceItem.cachedBluetoothDevice.address,
+                                    it.deviceItem.type,
+                                )
+                            }
+
+                            DeviceItemClick.Target.ACTION_ICON -> {
+                                deviceItemActionInteractor.onActionIconClick(it.deviceItem) { intent
+                                    ->
+                                    startSettingsActivity(intent, it.clickedView)
+                                }
+                            }
+                        }
                     }
                     .launchIn(this)
 
@@ -287,20 +300,6 @@
         )
     }
 
-    override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) {
-        uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED)
-        val intent =
-            Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply {
-                putExtra(
-                    EXTRA_SHOW_FRAGMENT_ARGUMENTS,
-                    Bundle().apply {
-                        putString("device_address", deviceItem.cachedBluetoothDevice.address)
-                    },
-                )
-            }
-        startSettingsActivity(intent, view)
-    }
-
     override fun onSeeAllClicked(view: View) {
         uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED)
         startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view)
@@ -382,8 +381,6 @@
 }
 
 interface BluetoothTileDialogCallback {
-    fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View)
-
     fun onSeeAllClicked(view: View)
 
     fun onPairNewDeviceClicked(view: View)
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
index 2ba4c73..f7af16d 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
@@ -53,5 +53,6 @@
     val background: Int? = null,
     var isEnabled: Boolean = true,
     var actionAccessibilityLabel: String = "",
-    var isActive: Boolean = false
+    var isActive: Boolean = false,
+    val actionIconRes: Int = -1,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
index 2b55e1c..cb4ec37 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.bluetooth.qsdialog
 
+import android.content.Intent
+import android.os.Bundle
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -25,7 +27,9 @@
 import kotlinx.coroutines.withContext
 
 interface DeviceItemActionInteractor {
-    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {}
+    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog)
+
+    suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit)
 }
 
 @SysUISingleton
@@ -67,4 +71,44 @@
             }
         }
     }
+
+    override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) {
+        withContext(backgroundDispatcher) {
+            deviceItem.cachedBluetoothDevice.apply {
+                when (deviceItem.type) {
+                    DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
+                    DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+                    DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
+                    DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
+                        uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED)
+                        val intent =
+                            Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply {
+                                putExtra(
+                                    EXTRA_SHOW_FRAGMENT_ARGUMENTS,
+                                    Bundle().apply {
+                                        putString(
+                                            "device_address",
+                                            deviceItem.cachedBluetoothDevice.address,
+                                        )
+                                    },
+                                )
+                            }
+                        onIntent(intent)
+                    }
+                    DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+                    DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                        throw IllegalArgumentException("Invalid device type: ${deviceItem.type}")
+                        // Throw exception. Should already be handled in
+                        // AudioSharingDeviceItemActionInteractor.
+                    }
+                }
+            }
+        }
+    }
+
+    private companion object {
+        const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
+        const val ACTION_BLUETOOTH_DEVICE_DETAILS =
+            "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 92f0580..095e6e7 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -30,6 +30,8 @@
 private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy
 private val connected = R.string.quick_settings_bluetooth_device_connected
 private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing
+private val audioSharingAddIcon = R.drawable.ic_add
+private val audioSharingOnGoingIcon = R.drawable.ic_check
 private val saved = R.string.quick_settings_bluetooth_device_saved
 private val actionAccessibilityLabelActivate =
     R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate
@@ -63,6 +65,7 @@
             background: Int,
             actionAccessibilityLabel: String,
             isActive: Boolean,
+            actionIconRes: Int = R.drawable.ic_settings_24dp,
         ): DeviceItem {
             return DeviceItem(
                 type = type,
@@ -75,6 +78,7 @@
                 isEnabled = !cachedDevice.isBusy,
                 actionAccessibilityLabel = actionAccessibilityLabel,
                 isActive = isActive,
+                actionIconRes = actionIconRes,
             )
         }
     }
@@ -125,6 +129,7 @@
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn,
             "",
             isActive = !cachedDevice.isBusy,
+            actionIconRes = audioSharingOnGoingIcon,
         )
     }
 }
@@ -156,6 +161,7 @@
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
             "",
             isActive = false,
+            actionIconRes = audioSharingAddIcon,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 00eead6..555fe6e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -31,6 +31,7 @@
 import com.android.systemui.statusbar.QsFrameTranslateModule;
 import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.wm.shell.appzoomout.AppZoomOut;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.desktopmode.DesktopMode;
@@ -115,6 +116,9 @@
         @BindsInstance
         Builder setDesktopMode(Optional<DesktopMode> d);
 
+        @BindsInstance
+        Builder setAppZoomOut(Optional<AppZoomOut> a);
+
         SysUIComponent build();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
index 41a59a9..ae62387 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.util.kotlin.FlowDumperImpl
@@ -50,6 +51,8 @@
 constructor(
     biometricSettingsRepository: BiometricSettingsRepository,
     deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor,
+    deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    keyguardBypassInteractor: KeyguardBypassInteractor,
     deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
     deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
     fingerprintPropertyRepository: FingerprintPropertyRepository,
@@ -82,7 +85,7 @@
                 emit(recentPowerButtonPressThresholdMs * -1L - 1L)
             }
 
-    val playSuccessHaptic: Flow<Unit> =
+    private val playHapticsOnDeviceEntry: Flow<Boolean> =
         deviceEntrySourceInteractor.deviceEntryFromBiometricSource
             .sample(
                 combine(
@@ -92,17 +95,29 @@
                     ::Triple,
                 )
             )
-            .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
+            .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
                 val sideFpsAllowsHaptic =
                     !powerButtonDown &&
                         systemClock.uptimeMillis() - lastPowerButtonWakeup >
                             recentPowerButtonPressThresholdMs
                 val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic
                 if (!allowHaptic) {
-                    logger.d("Skip success haptic. Recent power button press or button is down.")
+                    logger.d(
+                        "Skip success entry haptic from power button. Recent power button press or button is down."
+                    )
                 }
                 allowHaptic
             }
+
+    private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> =
+        deviceEntryFaceAuthInteractor.isAuthenticated
+            .filter { it }
+            .sample(keyguardBypassInteractor.isBypassAvailable)
+            .map { !it }
+
+    val playSuccessHaptic: Flow<Unit> =
+        merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled)
+            .filter { it }
             // map to Unit
             .map {}
             .dumpWhileCollecting("playSuccessHaptic")
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index 21002c6..d7a4dba 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -278,11 +278,11 @@
         }
 
     private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean {
-        val oobeLaunchTime =
-            tutorialRepository.getScheduledTutorialLaunchTime(deviceType) ?: return false
-        return clock
-            .instant()
-            .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds))
+        val oobeTime =
+            tutorialRepository.getScheduledTutorialLaunchTime(deviceType)
+                ?: tutorialRepository.getNotifiedTime(deviceType)
+                ?: return false
+        return clock.instant().isAfter(oobeTime.plusSeconds(initialDelayDuration.inWholeSeconds))
     }
 
     private data class StatsUpdateRequest(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
index 0954482..a6b9442 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
@@ -31,6 +31,7 @@
 import androidx.media3.session.MediaController as Media3Controller
 import androidx.media3.session.SessionCommand
 import androidx.media3.session.SessionToken
+import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -128,7 +129,11 @@
                     drawable,
                     null, // no action to perform when clicked
                     context.getString(R.string.controls_media_button_connecting),
-                    context.getDrawable(R.drawable.ic_media_connecting_container),
+                    if (Flags.mediaControlsUiUpdate()) {
+                        context.getDrawable(R.drawable.ic_media_connecting_status_container)
+                    } else {
+                        context.getDrawable(R.drawable.ic_media_connecting_container)
+                    },
                     // Specify a rebind id to prevent the spinner from restarting on later binds.
                     com.android.internal.R.drawable.progress_small_material,
                 )
@@ -230,17 +235,33 @@
                 Player.COMMAND_PLAY_PAUSE -> {
                     if (!controller.isPlaying) {
                         MediaAction(
-                            context.getDrawable(R.drawable.ic_media_play),
+                            if (Flags.mediaControlsUiUpdate()) {
+                                context.getDrawable(R.drawable.ic_media_play_button)
+                            } else {
+                                context.getDrawable(R.drawable.ic_media_play)
+                            },
                             { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) },
                             context.getString(R.string.controls_media_button_play),
-                            context.getDrawable(R.drawable.ic_media_play_container),
+                            if (Flags.mediaControlsUiUpdate()) {
+                                context.getDrawable(R.drawable.ic_media_play_button_container)
+                            } else {
+                                context.getDrawable(R.drawable.ic_media_play_container)
+                            },
                         )
                     } else {
                         MediaAction(
-                            context.getDrawable(R.drawable.ic_media_pause),
+                            if (Flags.mediaControlsUiUpdate()) {
+                                context.getDrawable(R.drawable.ic_media_pause_button)
+                            } else {
+                                context.getDrawable(R.drawable.ic_media_pause)
+                            },
                             { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) },
                             context.getString(R.string.controls_media_button_pause),
-                            context.getDrawable(R.drawable.ic_media_pause_container),
+                            if (Flags.mediaControlsUiUpdate()) {
+                                context.getDrawable(R.drawable.ic_media_pause_button_container)
+                            } else {
+                                context.getDrawable(R.drawable.ic_media_pause_container)
+                            },
                         )
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index 4f97913..9bf556c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -29,6 +29,7 @@
 import android.service.notification.StatusBarNotification
 import android.util.Log
 import androidx.media.utils.MediaConstants
+import com.android.systemui.Flags
 import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS
 import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS
 import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -69,7 +70,11 @@
                 drawable,
                 null, // no action to perform when clicked
                 context.getString(R.string.controls_media_button_connecting),
-                context.getDrawable(R.drawable.ic_media_connecting_container),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_connecting_status_container)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_connecting_container)
+                },
                 // Specify a rebind id to prevent the spinner from restarting on later binds.
                 com.android.internal.R.drawable.progress_small_material,
             )
@@ -157,18 +162,34 @@
     return when (action) {
         PlaybackState.ACTION_PLAY -> {
             MediaAction(
-                context.getDrawable(R.drawable.ic_media_play),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_play_button)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_play)
+                },
                 { controller.transportControls.play() },
                 context.getString(R.string.controls_media_button_play),
-                context.getDrawable(R.drawable.ic_media_play_container),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_play_button_container)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_play_container)
+                },
             )
         }
         PlaybackState.ACTION_PAUSE -> {
             MediaAction(
-                context.getDrawable(R.drawable.ic_media_pause),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_pause_button)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_pause)
+                },
                 { controller.transportControls.pause() },
                 context.getString(R.string.controls_media_button_pause),
-                context.getDrawable(R.drawable.ic_media_pause_container),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_pause_button_container)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_pause_container)
+                },
             )
         }
         PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
index 3928a71..a2ddc20 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
@@ -1016,9 +1016,24 @@
                 expandedLayout.load(context, R.xml.media_recommendations_expanded)
             }
         }
+        readjustPlayPauseWidth()
         refreshState()
     }
 
+    private fun readjustPlayPauseWidth() {
+        // TODO: move to xml file when flag is removed.
+        if (Flags.mediaControlsUiUpdate()) {
+            collapsedLayout.constrainWidth(
+                R.id.actionPlayPause,
+                context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width),
+            )
+            expandedLayout.constrainWidth(
+                R.id.actionPlayPause,
+                context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width),
+            )
+        }
+    }
+
     /** Get a view state based on the width and height set by the scene */
     private fun obtainSceneContainerViewState(state: MediaHostState?): TransitionViewState? {
         logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 2bcd3fc..10b726b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -92,6 +92,7 @@
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
 import javax.inject.Inject;
@@ -297,6 +298,9 @@
     // The last lock time. Uses currentTimeMillis
     @VisibleForTesting
     protected final AtomicLong mLastLockTime = new AtomicLong(-1);
+    // Whether or not the device is locked
+    @VisibleForTesting
+    protected final AtomicBoolean mLocked = new AtomicBoolean(true);
 
     protected int mCurrentUserId = 0;
 
@@ -369,6 +373,7 @@
                         if (!unlocked) {
                             mLastLockTime.set(System.currentTimeMillis());
                         }
+                        mLocked.set(!unlocked);
                     }));
         }
     }
@@ -737,7 +742,7 @@
             return false;
         }
 
-        if (!mKeyguardManager.isDeviceLocked()) {
+        if (!mLocked.get()) {
             return false;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 75117936..38f7c39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -34,6 +34,7 @@
 import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.TrackTracer
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags.spatialModelAppPushback
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -52,7 +53,9 @@
 import com.android.systemui.util.WallpaperController
 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
 import com.android.systemui.window.flag.WindowBlurFlag
+import com.android.wm.shell.appzoomout.AppZoomOut
 import java.io.PrintWriter
+import java.util.Optional
 import javax.inject.Inject
 import kotlin.math.max
 import kotlin.math.sign
@@ -79,6 +82,7 @@
     private val splitShadeStateController: SplitShadeStateController,
     private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor,
     @Application private val applicationScope: CoroutineScope,
+    private val appZoomOutOptional: Optional<AppZoomOut>,
     dumpManager: DumpManager,
     configurationController: ConfigurationController,
 ) : ShadeExpansionListener, Dumpable {
@@ -271,6 +275,13 @@
     private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) {
         lastAppliedBlur = appliedBlurRadius
         wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius)
+        if (spatialModelAppPushback()) {
+            appZoomOutOptional.ifPresent { appZoomOut ->
+                appZoomOut.setProgress(
+                    zoomOutFromShadeRadius
+                )
+            }
+        }
         listeners.forEach {
             it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius)
             it.onBlurRadiusChanged(appliedBlurRadius)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 48cf7a83..155049f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -23,6 +23,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
@@ -30,6 +31,7 @@
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
@@ -1014,12 +1016,24 @@
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         if (mInteractive) {
+            // Add two accessibility actions that both performs expanding the notification shade
             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
-            AccessibilityNodeInfo.AccessibilityAction unlock
-                    = new AccessibilityNodeInfo.AccessibilityAction(
+
+            AccessibilityAction seeAll = new AccessibilityAction(
                     AccessibilityNodeInfo.ACTION_CLICK,
-                    getContext().getString(R.string.accessibility_overflow_action));
-            info.addAction(unlock);
+                    getContext().getString(R.string.accessibility_overflow_action)
+            );
+            info.addAction(seeAll);
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle args) {
+        // override ACTION_EXPAND with ACTION_CLICK
+        if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+            return super.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, args);
+        } else {
+            return super.performAccessibilityAction(action, args);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
index 10e67a4..640d364 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
@@ -25,7 +25,7 @@
 import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED
 import android.content.Context
 import android.graphics.drawable.Drawable
-import android.text.TextUtils
+import android.text.TextUtils.isEmpty
 import android.transition.AutoTransition
 import android.transition.Transition
 import android.transition.TransitionManager
@@ -37,13 +37,10 @@
 import android.widget.Switch
 import android.widget.TextView
 import com.android.settingslib.Utils
-
 import com.android.systemui.res.R
 import com.android.systemui.util.Assert
 
-/**
- * Half-shelf for notification channel controls
- */
+/** Half-shelf for notification channel controls */
 class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
     lateinit var controller: ChannelEditorDialogController
     var appIcon: Drawable? = null
@@ -84,23 +81,21 @@
 
         val transition = AutoTransition()
         transition.duration = 200
-        transition.addListener(object : Transition.TransitionListener {
-            override fun onTransitionEnd(p0: Transition?) {
-                notifySubtreeAccessibilityStateChangedIfNeeded()
-            }
+        transition.addListener(
+            object : Transition.TransitionListener {
+                override fun onTransitionEnd(p0: Transition?) {
+                    notifySubtreeAccessibilityStateChangedIfNeeded()
+                }
 
-            override fun onTransitionResume(p0: Transition?) {
-            }
+                override fun onTransitionResume(p0: Transition?) {}
 
-            override fun onTransitionPause(p0: Transition?) {
-            }
+                override fun onTransitionPause(p0: Transition?) {}
 
-            override fun onTransitionCancel(p0: Transition?) {
-            }
+                override fun onTransitionCancel(p0: Transition?) {}
 
-            override fun onTransitionStart(p0: Transition?) {
+                override fun onTransitionStart(p0: Transition?) {}
             }
-        })
+        )
         TransitionManager.beginDelayedTransition(this, transition)
 
         // Remove any rows
@@ -130,8 +125,9 @@
 
     private fun updateAppControlRow(enabled: Boolean) {
         appControlRow.iconView.setImageDrawable(appIcon)
-        appControlRow.channelName.text = context.resources
-                .getString(R.string.notification_channel_dialog_title, appName)
+        val title = context.resources.getString(R.string.notification_channel_dialog_title, appName)
+        appControlRow.channelName.text = title
+        appControlRow.switch.contentDescription = title
         appControlRow.switch.isChecked = enabled
         appControlRow.switch.setOnCheckedChangeListener { _, b ->
             controller.proposeSetAppNotificationsEnabled(b)
@@ -164,8 +160,8 @@
     var gentle = false
 
     init {
-        highlightColor = Utils.getColorAttrDefaultColor(
-                context, android.R.attr.colorControlHighlight)
+        highlightColor =
+            Utils.getColorAttrDefaultColor(context, android.R.attr.colorControlHighlight)
     }
 
     var channel: NotificationChannel? = null
@@ -182,17 +178,16 @@
         switch = requireViewById(R.id.toggle)
         switch.setOnCheckedChangeListener { _, b ->
             channel?.let {
-                controller.proposeEditForChannel(it,
-                        if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW)
-                        else IMPORTANCE_NONE)
+                controller.proposeEditForChannel(
+                    it,
+                    if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) else IMPORTANCE_NONE,
+                )
             }
         }
         setOnClickListener { switch.toggle() }
     }
 
-    /**
-     * Play an animation that highlights this row
-     */
+    /** Play an animation that highlights this row */
     fun playHighlight() {
         // Use 0 for the start value because our background is given to us by our parent
         val fadeInLoop = ValueAnimator.ofObject(ArgbEvaluator(), 0, highlightColor)
@@ -211,17 +206,21 @@
 
         channelName.text = nc.name ?: ""
 
-        nc.group?.let { groupId ->
-            channelDescription.text = controller.groupNameForId(groupId)
-        }
+        nc.group?.let { groupId -> channelDescription.text = controller.groupNameForId(groupId) }
 
-        if (nc.group == null || TextUtils.isEmpty(channelDescription.text)) {
+        if (nc.group == null || isEmpty(channelDescription.text)) {
             channelDescription.visibility = View.GONE
         } else {
             channelDescription.visibility = View.VISIBLE
         }
 
         switch.isChecked = nc.importance != IMPORTANCE_NONE
+        switch.contentDescription =
+            if (isEmpty(channelDescription.text)) {
+                channelName.text
+            } else {
+                "${channelName.text} ${channelDescription.text}"
+            }
     }
 
     private fun updateImportance() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
new file mode 100644
index 0000000..e27ff7d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.statusbar.notification.row
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.drawable.Drawable
+
+/**
+ * A background style for smarter-smart-actions.
+ *
+ * TODO(b/383567383) implement final UX
+ */
+class MagicActionBackgroundDrawable(context: Context) : Drawable() {
+
+    private var _alpha: Int = 255
+    private var _colorFilter: ColorFilter? = null
+    private val paint =
+        Paint().apply {
+            color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
+        }
+
+    override fun draw(canvas: Canvas) {
+        canvas.drawRect(bounds, paint)
+    }
+
+    override fun setAlpha(alpha: Int) {
+        _alpha = alpha
+        invalidateSelf()
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        _colorFilter = colorFilter
+        invalidateSelf()
+    }
+
+    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 7c44eae..70e27a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar.notification.row;
 
-import static android.app.Flags.notificationsRedesignTemplates;
-
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED;
@@ -481,16 +479,15 @@
                     logger.logAsyncTaskProgress(entryForLogging,
                             "creating low-priority group summary remote view");
                     result.mNewMinimizedGroupHeaderView =
-                            builder.makeLowPriorityContentView(/* useRegularSubtext = */ true,
-                                    /* highlightExpander = */ notificationsRedesignTemplates());
+                            builder.makeLowPriorityContentView(true /* useRegularSubtext */);
                 }
             }
             setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider);
             result.packageContext = packageContext;
             result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(
-                    /* showingPublic = */ false);
+                    false /* showingPublic */);
             result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
-                    /* showingPublic = */ true);
+                    true /* showingPublic */);
 
             return result;
         });
@@ -1139,8 +1136,7 @@
     private static RemoteViews createContentView(Notification.Builder builder,
             boolean isMinimized, boolean useLarge) {
         if (isMinimized) {
-            return builder.makeLowPriorityContentView(/* useRegularSubtext = */ false,
-                    /* highlightExpander = */ false);
+            return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
         }
         return builder.createContentView(useLarge);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index ae9b69c..c619b17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -16,7 +16,6 @@
 package com.android.systemui.statusbar.notification.row
 
 import android.annotation.SuppressLint
-import android.app.Flags.notificationsRedesignTemplates
 import android.app.Notification
 import android.app.Notification.MessagingStyle
 import android.content.Context
@@ -888,10 +887,7 @@
                             entryForLogging,
                             "creating low-priority group summary remote view",
                         )
-                        builder.makeLowPriorityContentView(
-                            /* useRegularSubtext = */ true,
-                            /* highlightExpander = */ notificationsRedesignTemplates(),
-                        )
+                        builder.makeLowPriorityContentView(true /* useRegularSubtext */)
                     } else null
                 NewRemoteViews(
                         contracted = contracted,
@@ -1661,10 +1657,7 @@
             useLarge: Boolean,
         ): RemoteViews {
             return if (isMinimized) {
-                builder.makeLowPriorityContentView(
-                    /* useRegularSubtext = */ false,
-                    /* highlightExpander = */ false,
-                )
+                builder.makeLowPriorityContentView(false /* useRegularSubtext */)
             } else builder.createContentView(useLarge)
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index e477c74..8e48065 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -568,8 +568,7 @@
                 builder = Notification.Builder.recoverBuilder(getContext(),
                         notification.getNotification());
             }
-            header = builder.makeLowPriorityContentView(true /* useRegularSubtext */,
-                    notificationsRedesignTemplates() /* highlightExpander */);
+            header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
             if (mMinimizedGroupHeader == null) {
                 mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(),
                         this);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
index 56c9e9a..cb26679 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
@@ -41,6 +41,7 @@
 import android.view.accessibility.AccessibilityNodeInfo
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
 import android.widget.Button
+import com.android.systemui.Flags
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.shared.system.ActivityManagerWrapper
@@ -52,6 +53,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
 import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions
 import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions
@@ -400,6 +402,15 @@
             .apply {
                 text = action.title
 
+                if (Flags.notificationMagicActionsTreatment()) {
+                    if (
+                        smartActions.fromAssistant &&
+                            action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false)
+                    ) {
+                        background = MagicActionBackgroundDrawable(parent.context)
+                    }
+                }
+
                 // We received the Icon from the application - so use the Context of the application
                 // to
                 // reference icon resources.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
index 5d622ea..e61acc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
@@ -224,6 +225,30 @@
         }
     }
 
+    @Test
+    fun testOnActionIconClick_audioSharingMediaDevice_stopBroadcast() {
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                actionInteractorImpl.onActionIconClick(inAudioSharingMediaDeviceItem) {}
+                assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted)
+                    .isEqualTo(false)
+            }
+        }
+    }
+
+    @Test
+    fun testOnActionIconClick_availableAudioSharingMediaDevice_startBroadcast() {
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                actionInteractorImpl.onActionIconClick(connectedAudioSharingMediaDeviceItem) {}
+                assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted)
+                    .isEqualTo(true)
+            }
+        }
+    }
+
     private companion object {
         const val DEVICE_NAME = "device"
         const val DEVICE_CONNECTION_SUMMARY = "active"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
index 6bfd080..4396b0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
@@ -32,6 +32,9 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.SysUiState
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.shadeDialogContextInteractor
@@ -43,9 +46,8 @@
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineScheduler
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
@@ -93,7 +95,6 @@
 
     private val fakeSystemClock = FakeSystemClock()
 
-    private lateinit var scheduler: TestCoroutineScheduler
     private lateinit var dispatcher: CoroutineDispatcher
     private lateinit var testScope: TestScope
     private lateinit var icon: Pair<Drawable, String>
@@ -104,9 +105,8 @@
 
     @Before
     fun setUp() {
-        scheduler = TestCoroutineScheduler()
-        dispatcher = UnconfinedTestDispatcher(scheduler)
-        testScope = TestScope(dispatcher)
+        dispatcher = kosmos.testDispatcher
+        testScope = kosmos.testScope
 
         whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
 
@@ -124,23 +124,19 @@
                 kosmos.shadeDialogContextInteractor,
             )
 
-        whenever(
-            sysuiDialogFactory.create(
-                any(SystemUIDialog.Delegate::class.java),
-                any()
-            )
-        ).thenAnswer {
-            SystemUIDialog(
-                mContext,
-                0,
-                SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
-                dialogManager,
-                sysuiState,
-                fakeBroadcastDispatcher,
-                dialogTransitionAnimator,
-                it.getArgument(0),
-            )
-        }
+        whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any()))
+            .thenAnswer {
+                SystemUIDialog(
+                    mContext,
+                    0,
+                    SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
+                    dialogManager,
+                    sysuiState,
+                    fakeBroadcastDispatcher,
+                    dialogTransitionAnimator,
+                    it.getArgument(0),
+                )
+            }
 
         icon = Pair(drawable, DEVICE_NAME)
         deviceItem =
@@ -194,20 +190,29 @@
 
     @Test
     fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
-        deviceItem.isEnabled = true
+        testScope.runTest {
+            deviceItem.isEnabled = true
 
-        val view =
-            LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
-        val viewHolder =
-            mBluetoothTileDialogDelegate
-                .Adapter(bluetoothTileDialogCallback)
-                .DeviceItemViewHolder(view)
-        viewHolder.bind(deviceItem, bluetoothTileDialogCallback)
-        val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+            val view =
+                LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+            val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
+            viewHolder.bind(deviceItem)
+            val container = view.requireViewById<View>(R.id.bluetooth_device_row)
 
-        assertThat(container).isNotNull()
-        assertThat(container.isEnabled).isTrue()
-        assertThat(container.hasOnClickListeners()).isTrue()
+            assertThat(container).isNotNull()
+            assertThat(container.isEnabled).isTrue()
+            assertThat(container.hasOnClickListeners()).isTrue()
+            val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
+            runCurrent()
+            container.performClick()
+            runCurrent()
+            assertThat(value).isNotNull()
+            value?.let {
+                assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW)
+                assertThat(it.clickedView).isEqualTo(container)
+                assertThat(it.deviceItem).isEqualTo(deviceItem)
+            }
+        }
     }
 
     @Test
@@ -229,9 +234,9 @@
                     sysuiDialogFactory,
                     kosmos.shadeDialogContextInteractor,
                 )
-                .Adapter(bluetoothTileDialogCallback)
+                .Adapter()
                 .DeviceItemViewHolder(view)
-        viewHolder.bind(deviceItem, bluetoothTileDialogCallback)
+        viewHolder.bind(deviceItem)
         val container = view.requireViewById<View>(R.id.bluetooth_device_row)
 
         assertThat(container).isNotNull()
@@ -240,6 +245,32 @@
     }
 
     @Test
+    fun testDeviceItemViewHolder_clickActionIcon() {
+        testScope.runTest {
+            deviceItem.isEnabled = true
+
+            val view =
+                LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+            val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
+            viewHolder.bind(deviceItem)
+            val actionIconView = view.requireViewById<View>(R.id.gear_icon)
+
+            assertThat(actionIconView).isNotNull()
+            assertThat(actionIconView.hasOnClickListeners()).isTrue()
+            val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
+            runCurrent()
+            actionIconView.performClick()
+            runCurrent()
+            assertThat(value).isNotNull()
+            value?.let {
+                assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON)
+                assertThat(it.clickedView).isEqualTo(actionIconView)
+                assertThat(it.deviceItem).isEqualTo(deviceItem)
+            }
+        }
+    }
+
+    @Test
     fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
         testScope.runTest {
             val dialog = mBluetoothTileDialogDelegate.createDialog()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index 5bf1513..0aa5199 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -118,6 +118,7 @@
             .isEqualTo(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
         assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice)
         assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME)
+        assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_add)
         assertThat(deviceItem.isActive).isFalse()
         assertThat(deviceItem.connectionSummary)
             .isEqualTo(
@@ -292,6 +293,7 @@
         assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice)
         assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME)
         assertThat(deviceItem.connectionSummary).isEqualTo(CONNECTION_SUMMARY)
+        assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_settings_24dp)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
index 25d1c37..7ed7361 100644
--- a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
+++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
@@ -435,6 +435,8 @@
 
     override fun unbundleNotification(key: String) {}
 
+    override fun rebundleNotification(key: String) {}
+
     companion object {
         const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
         const val SECONDARY_DISPLAY_ID = 2
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
index a839f17..c744eac 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
@@ -33,6 +33,9 @@
     var sourceAdded: Boolean = false
         private set
 
+    var audioSharingStarted: Boolean = false
+        private set
+
     private var profile: LocalBluetoothLeBroadcast? = null
 
     override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
@@ -50,7 +53,13 @@
 
     override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
 
-    override suspend fun startAudioSharing() {}
+    override suspend fun startAudioSharing() {
+        audioSharingStarted = true
+    }
+
+    override suspend fun stopAudioSharing() {
+        audioSharingStarted = false
+    }
 
     fun setAudioSharingAvailable(available: Boolean) {
         mutableAvailable = available
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
index 2a7e3e9..490b89b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.keyevent.domain.interactor.keyEventInteractor
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.util.time.systemClock
@@ -34,6 +35,8 @@
         DeviceEntryHapticsInteractor(
             biometricSettingsRepository = biometricSettingsRepository,
             deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor,
+            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+            keyguardBypassInteractor = keyguardBypassInteractor,
             deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
             deviceEntrySourceInteractor = deviceEntrySourceInteractor,
             fingerprintPropertyRepository = fingerprintPropertyRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index afe4821..439df54 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -54,8 +54,8 @@
  * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in
  * that Kosmos instance
  */
-fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) {
-    testScope.runTestWithSnapshots testBody@{ this@runTest.testBody() }
+fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = let { kosmos ->
+    testScope.runTestWithSnapshots { kosmos.testBody() }
 }
 
 fun Kosmos.runCurrent() = testScope.runCurrent()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
index 1ba5ddb..fc0c92e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
@@ -20,5 +20,5 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.zenModeRepository by Fixture { fakeZenModeRepository }
+var Kosmos.zenModeRepository by Fixture { fakeZenModeRepository }
 val Kosmos.fakeZenModeRepository by Fixture { FakeZenModeRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index ed5322e..db19d6e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -39,7 +39,7 @@
 
 val Kosmos.mediaOutputActionsInteractor by
     Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogManager) }
-val Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() }
+var Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() }
 val Kosmos.mediaOutputInteractor by
     Kosmos.Fixture {
         MediaOutputInteractor(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
index 712ec41..3f2b479 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
@@ -19,4 +19,4 @@
 import com.android.systemui.kosmos.Kosmos
 
 val Kosmos.fakeAudioRepository by Kosmos.Fixture { FakeAudioRepository() }
-val Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository }
+var Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt
new file mode 100644
index 0000000..e243193
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.dagger.volumeDialogComponentFactory
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+
+val Kosmos.volumeDialog by
+    Kosmos.Fixture {
+        VolumeDialog(
+            context = applicationContext,
+            visibilityInteractor = volumeDialogVisibilityInteractor,
+            componentFactory = volumeDialogComponentFactory,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt
new file mode 100644
index 0000000..73e5d8d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.dagger
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent
+import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory
+import com.android.systemui.volume.dialog.ui.binder.VolumeDialogViewBinder
+import com.android.systemui.volume.dialog.ui.binder.volumeDialogViewBinder
+import kotlinx.coroutines.CoroutineScope
+
+val Kosmos.volumeDialogComponentFactory by
+    Kosmos.Fixture {
+        object : VolumeDialogComponent.Factory {
+            override fun create(scope: CoroutineScope): VolumeDialogComponent =
+                volumeDialogComponent
+        }
+    }
+val Kosmos.volumeDialogComponent by
+    Kosmos.Fixture {
+        object : VolumeDialogComponent {
+            override fun volumeDialogViewBinder(): VolumeDialogViewBinder = volumeDialogViewBinder
+
+            override fun sliderComponentFactory(): VolumeDialogSliderComponent.Factory =
+                volumeDialogSliderComponentFactory
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
index 291dfc0..3d5698b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
@@ -19,4 +19,4 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository
 
-val Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() }
+var Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
index db9c48d..8f122b5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.volume.dialog.domain.interactor
 
-import android.os.Handler
-import android.os.looper
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.volumeDialogController
@@ -27,6 +25,6 @@
         VolumeDialogCallbacksInteractor(
             volumeDialogController = volumeDialogController,
             coroutineScope = applicationCoroutineScope,
-            bgHandler = Handler(looper),
+            bgHandler = null,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt
new file mode 100644
index 0000000..7cbdc3d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ringer
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder
+import com.android.systemui.volume.dialog.ringer.ui.viewmodel.volumeDialogRingerDrawerViewModel
+
+val Kosmos.volumeDialogRingerViewBinder by
+    Kosmos.Fixture { VolumeDialogRingerViewBinder(volumeDialogRingerDrawerViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
index 44371b4..cf357b4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
@@ -20,5 +20,5 @@
 
 val Kosmos.fakeVolumeDialogRingerFeedbackRepository by
     Kosmos.Fixture { FakeVolumeDialogRingerFeedbackRepository() }
-val Kosmos.volumeDialogRingerFeedbackRepository by
+var Kosmos.volumeDialogRingerFeedbackRepository: VolumeDialogRingerFeedbackRepository by
     Kosmos.Fixture { fakeVolumeDialogRingerFeedbackRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
index a494d04..4bebf89 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.plugins.volumeDialogController
 import com.android.systemui.volume.data.repository.audioSystemRepository
 import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
-import com.android.systemui.volume.dialog.ringer.data.repository.fakeVolumeDialogRingerFeedbackRepository
+import com.android.systemui.volume.dialog.ringer.data.repository.volumeDialogRingerFeedbackRepository
 
 val Kosmos.volumeDialogRingerInteractor by
     Kosmos.Fixture {
@@ -30,6 +30,6 @@
             volumeDialogStateInteractor = volumeDialogStateInteractor,
             controller = volumeDialogController,
             audioSystemRepository = audioSystemRepository,
-            ringerFeedbackRepository = fakeVolumeDialogRingerFeedbackRepository,
+            ringerFeedbackRepository = volumeDialogRingerFeedbackRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt
new file mode 100644
index 0000000..26b8bca
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.settings.domain
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.deviceProvisionedController
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
+
+val Kosmos.volumeDialogSettingsButtonInteractor by
+    Kosmos.Fixture {
+        VolumeDialogSettingsButtonInteractor(
+            applicationCoroutineScope,
+            deviceProvisionedController,
+            volumePanelGlobalStateInteractor,
+            volumeDialogVisibilityInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt
new file mode 100644
index 0000000..f9e128d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.settings.ui.binder
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.settings.ui.viewmodel.volumeDialogSettingsButtonViewModel
+
+val Kosmos.volumeDialogSettingsButtonViewBinder by
+    Kosmos.Fixture { VolumeDialogSettingsButtonViewBinder(volumeDialogSettingsButtonViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt
new file mode 100644
index 0000000..0ae3b03
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.settings.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.volume.dialog.settings.domain.volumeDialogSettingsButtonInteractor
+import com.android.systemui.volume.mediaDeviceSessionInteractor
+import com.android.systemui.volume.mediaOutputInteractor
+
+val Kosmos.volumeDialogSettingsButtonViewModel by
+    Kosmos.Fixture {
+        VolumeDialogSettingsButtonViewModel(
+            applicationContext,
+            testScope.testScheduler,
+            applicationCoroutineScope,
+            mediaOutputInteractor,
+            mediaDeviceSessionInteractor,
+            volumeDialogSettingsButtonInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
new file mode 100644
index 0000000..4f79f7b4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.dagger
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor
+
+private val Kosmos.mutableSliderComponentKosmoses: MutableMap<VolumeDialogSliderType, Kosmos> by
+    Kosmos.Fixture { mutableMapOf() }
+
+val Kosmos.volumeDialogSliderComponentFactory by
+    Kosmos.Fixture {
+        object : VolumeDialogSliderComponent.Factory {
+            override fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderComponent =
+                volumeDialogSliderComponent(sliderType)
+        }
+    }
+
+fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDialogSliderComponent {
+    return object : VolumeDialogSliderComponent {
+
+        private val localKosmos
+            get() =
+                mutableSliderComponentKosmoses.getOrPut(type) {
+                    Kosmos().also {
+                        it.setupVolumeDialogSliderComponent(this@volumeDialogSliderComponent, type)
+                    }
+                }
+
+        override fun sliderViewBinder(): VolumeDialogSliderViewBinder =
+            localKosmos.volumeDialogSliderViewBinder
+
+        override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder =
+            localKosmos.volumeDialogSliderHapticsViewBinder
+
+        override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder =
+            localKosmos.volumeDialogOverscrollViewBinder
+    }
+}
+
+private fun Kosmos.setupVolumeDialogSliderComponent(
+    parentKosmos: Kosmos,
+    type: VolumeDialogSliderType,
+) {
+    volumeDialogSliderType = type
+    applicationContext = parentKosmos.applicationContext
+    testScope = parentKosmos.testScope
+
+    volumeDialogController = parentKosmos.volumeDialogController
+    mediaControllerInteractor = parentKosmos.mediaControllerInteractor
+    mediaControllerRepository = parentKosmos.mediaControllerRepository
+    zenModeRepository = parentKosmos.zenModeRepository
+    volumeDialogVisibilityRepository = parentKosmos.volumeDialogVisibilityRepository
+    audioRepository = parentKosmos.audioRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt
new file mode 100644
index 0000000..13d6ca9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogOverscrollViewModel
+
+val Kosmos.volumeDialogOverscrollViewBinder by
+    Kosmos.Fixture { VolumeDialogOverscrollViewBinder(volumeDialogOverscrollViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
new file mode 100644
index 0000000..d6845b1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.haptics.msdl.msdlPlayer
+import com.android.systemui.haptics.vibratorHelper
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
+
+val Kosmos.volumeDialogSliderHapticsViewBinder by
+    Kosmos.Fixture {
+        VolumeDialogSliderHapticsViewBinder(
+            volumeDialogSliderInputEventsViewModel,
+            vibratorHelper,
+            msdlPlayer,
+            systemClock,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
new file mode 100644
index 0000000..c6db717
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel
+
+val Kosmos.volumeDialogSliderViewBinder by
+    Kosmos.Fixture {
+        VolumeDialogSliderViewBinder(
+            volumeDialogSliderViewModel,
+            volumeDialogSliderInputEventsViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt
new file mode 100644
index 0000000..83527d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSlidersViewModel
+
+val Kosmos.volumeDialogSlidersViewBinder by
+    Kosmos.Fixture { VolumeDialogSlidersViewBinder(volumeDialogSlidersViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt
new file mode 100644
index 0000000..fe2f3d8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor
+
+val Kosmos.volumeDialogOverscrollViewModel by
+    Kosmos.Fixture {
+        VolumeDialogOverscrollViewModel(applicationContext, volumeDialogSliderInputEventsInteractor)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt
new file mode 100644
index 0000000..09f9f1c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.volume.domain.interactor.audioVolumeInteractor
+
+val Kosmos.volumeDialogSliderIconProvider by
+    Kosmos.Fixture {
+        VolumeDialogSliderIconProvider(
+            context = applicationContext,
+            audioVolumeInteractor = audioVolumeInteractor,
+            zenModeInteractor = zenModeInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt
new file mode 100644
index 0000000..2de0e8f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor
+
+val Kosmos.volumeDialogSliderInputEventsViewModel by
+    Kosmos.Fixture {
+        VolumeDialogSliderInputEventsViewModel(
+            applicationCoroutineScope,
+            volumeDialogSliderInputEventsInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
new file mode 100644
index 0000000..63cd440
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInteractor
+
+val Kosmos.volumeDialogSliderViewModel by
+    Kosmos.Fixture {
+        VolumeDialogSliderViewModel(
+            interactor = volumeDialogSliderInteractor,
+            visibilityInteractor = volumeDialogVisibilityInteractor,
+            coroutineScope = applicationCoroutineScope,
+            volumeDialogSliderIconProvider = volumeDialogSliderIconProvider,
+            systemClock = systemClock,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt
new file mode 100644
index 0000000..5531f76
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor
+
+val Kosmos.volumeDialogSlidersViewModel by
+    Kosmos.Fixture {
+        VolumeDialogSlidersViewModel(
+            applicationCoroutineScope,
+            volumeDialogSlidersInteractor,
+            volumeDialogSliderComponentFactory,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt
new file mode 100644
index 0000000..dc09e32
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.binder
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder
+import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSlidersViewBinder
+import com.android.systemui.volume.dialog.ui.utils.jankListenerFactory
+import com.android.systemui.volume.dialog.ui.viewmodel.volumeDialogViewModel
+import com.android.systemui.volume.dialog.utils.volumeTracer
+
+val Kosmos.volumeDialogViewBinder by
+    Kosmos.Fixture {
+        VolumeDialogViewBinder(
+            applicationContext.resources,
+            volumeDialogViewModel,
+            jankListenerFactory,
+            volumeTracer,
+            volumeDialogRingerViewBinder,
+            volumeDialogSlidersViewBinder,
+            volumeDialogSettingsButtonViewBinder,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt
new file mode 100644
index 0000000..35ec5d3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.utils
+
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.jankListenerFactory by Kosmos.Fixture { JankListenerFactory(interactionJankMonitor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt
new file mode 100644
index 0000000..05ef462
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.configurationController
+import com.android.systemui.statusbar.policy.devicePostureController
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor
+
+val Kosmos.volumeDialogViewModel by
+    Kosmos.Fixture {
+        VolumeDialogViewModel(
+            applicationContext,
+            volumeDialogVisibilityInteractor,
+            volumeDialogSlidersInteractor,
+            volumeDialogStateInteractor,
+            devicePostureController,
+            configurationController,
+        )
+    }
diff --git a/packages/Vcn/service-b/Android.bp b/packages/Vcn/service-b/Android.bp
index 1370b06..97574e6 100644
--- a/packages/Vcn/service-b/Android.bp
+++ b/packages/Vcn/service-b/Android.bp
@@ -39,9 +39,7 @@
     name: "connectivity-utils-service-vcn-internal",
     sdk_version: "module_current",
     min_sdk_version: "30",
-    srcs: [
-        ":framework-connectivity-shared-srcs",
-    ],
+    srcs: ["service-utils/**/*.java"],
     libs: [
         "framework-annotations-lib",
         "unsupportedappusage",
diff --git a/packages/Vcn/service-b/service-utils/android/util/LocalLog.java b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java
new file mode 100644
index 0000000..5955d93
--- /dev/null
+++ b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java
@@ -0,0 +1,148 @@
+/*
+ * 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 android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.SystemClock;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+
+/**
+ * @hide
+ */
+// Exported to Mainline modules; cannot use annotations
+// @android.ravenwood.annotation.RavenwoodKeepWholeClass
+// TODO: b/374174952 This is an exact copy of frameworks/base/core/java/android/util/LocalLog.java.
+// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag
+// is fully ramped. When the flag is fully ramped and the development is finalized, this file can
+// be removed.
+public final class LocalLog {
+
+    private final Deque<String> mLog;
+    private final int mMaxLines;
+
+    /**
+     * {@code true} to use log timestamps expressed in local date/time, {@code false} to use log
+     * timestamped expressed with the elapsed realtime clock and UTC system clock. {@code false} is
+     * useful when logging behavior that modifies device time zone or system clock.
+     */
+    private final boolean mUseLocalTimestamps;
+
+    @UnsupportedAppUsage
+    public LocalLog(int maxLines) {
+        this(maxLines, true /* useLocalTimestamps */);
+    }
+
+    public LocalLog(int maxLines, boolean useLocalTimestamps) {
+        mMaxLines = Math.max(0, maxLines);
+        mLog = new ArrayDeque<>(mMaxLines);
+        mUseLocalTimestamps = useLocalTimestamps;
+    }
+
+    @UnsupportedAppUsage
+    public void log(String msg) {
+        if (mMaxLines <= 0) {
+            return;
+        }
+        final String logLine;
+        if (mUseLocalTimestamps) {
+            logLine = LocalDateTime.now() + " - " + msg;
+        } else {
+            logLine = Duration.ofMillis(SystemClock.elapsedRealtime())
+                    + " / " + Instant.now() + " - " + msg;
+        }
+        append(logLine);
+    }
+
+    private synchronized void append(String logLine) {
+        while (mLog.size() >= mMaxLines) {
+            mLog.remove();
+        }
+        mLog.add(logLine);
+    }
+
+    @UnsupportedAppUsage
+    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        dump(pw);
+    }
+
+    public synchronized void dump(PrintWriter pw) {
+        dump("", pw);
+    }
+
+    /**
+     * Dumps the content of local log to print writer with each log entry predeced with indent
+     *
+     * @param indent indent that precedes each log entry
+     * @param pw printer writer to write into
+     */
+    public synchronized void dump(String indent, PrintWriter pw) {
+        Iterator<String> itr = mLog.iterator();
+        while (itr.hasNext()) {
+            pw.printf("%s%s\n", indent, itr.next());
+        }
+    }
+
+    public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        reverseDump(pw);
+    }
+
+    public synchronized void reverseDump(PrintWriter pw) {
+        Iterator<String> itr = mLog.descendingIterator();
+        while (itr.hasNext()) {
+            pw.println(itr.next());
+        }
+    }
+
+    // @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public synchronized void clear() {
+        mLog.clear();
+    }
+
+    public static class ReadOnlyLocalLog {
+        private final LocalLog mLog;
+        ReadOnlyLocalLog(LocalLog log) {
+            mLog = log;
+        }
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            mLog.dump(pw);
+        }
+        public void dump(PrintWriter pw) {
+            mLog.dump(pw);
+        }
+        public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            mLog.reverseDump(pw);
+        }
+        public void reverseDump(PrintWriter pw) {
+            mLog.reverseDump(pw);
+        }
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public ReadOnlyLocalLog readOnlyLocalLog() {
+        return new ReadOnlyLocalLog(this);
+    }
+}
\ No newline at end of file
diff --git a/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java
new file mode 100644
index 0000000..7db62f8
--- /dev/null
+++ b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java
@@ -0,0 +1,142 @@
+/*
+ * 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.internal.util;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+ /**
+ * An AlarmListener that sends the specified message to a Handler and keeps the system awake until
+ * the message is processed.
+ *
+ * This is useful when using the AlarmManager direct callback interface to wake up the system and
+ * request that an object whose API consists of messages (such as a StateMachine) perform some
+ * action.
+ *
+ * In this situation, using AlarmManager.onAlarmListener by itself will wake up the system to send
+ * the message, but does not guarantee that the system will be awake until the target object has
+ * processed it. This is because as soon as the onAlarmListener sends the message and returns, the
+ * AlarmManager releases its wakelock and the system is free to go to sleep again.
+ */
+// TODO: b/374174952 This is an exact copy of
+// frameworks/base/core/java/com/android/internal/util/WakeupMessage.java.
+// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag
+// is fully ramped. When the flag is fully ramped and the development is finalized, this file can
+// be removed.
+public class WakeupMessage implements AlarmManager.OnAlarmListener {
+    private final AlarmManager mAlarmManager;
+
+    @VisibleForTesting
+    protected final Handler mHandler;
+    @VisibleForTesting
+    protected final String mCmdName;
+    @VisibleForTesting
+    protected final int mCmd, mArg1, mArg2;
+    @VisibleForTesting
+    protected final Object mObj;
+    private final Runnable mRunnable;
+    private boolean mScheduled;
+
+    public WakeupMessage(Context context, Handler handler,
+            String cmdName, int cmd, int arg1, int arg2, Object obj) {
+        mAlarmManager = getAlarmManager(context);
+        mHandler = handler;
+        mCmdName = cmdName;
+        mCmd = cmd;
+        mArg1 = arg1;
+        mArg2 = arg2;
+        mObj = obj;
+        mRunnable = null;
+    }
+
+    public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) {
+        this(context, handler, cmdName, cmd, arg1, 0, null);
+    }
+
+    public WakeupMessage(Context context, Handler handler,
+            String cmdName, int cmd, int arg1, int arg2) {
+        this(context, handler, cmdName, cmd, arg1, arg2, null);
+    }
+
+    public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) {
+        this(context, handler, cmdName, cmd, 0, 0, null);
+    }
+
+    public WakeupMessage(Context context, Handler handler, String cmdName, Runnable runnable) {
+        mAlarmManager = getAlarmManager(context);
+        mHandler = handler;
+        mCmdName = cmdName;
+        mCmd = 0;
+        mArg1 = 0;
+        mArg2 = 0;
+        mObj = null;
+        mRunnable = runnable;
+    }
+
+    private static AlarmManager getAlarmManager(Context context) {
+        return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+    }
+
+    /**
+     * Schedule the message to be delivered at the time in milliseconds of the
+     * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup
+     * the device when it goes off. If schedule is called multiple times without the message being
+     * dispatched then the alarm is rescheduled to the new time.
+     */
+    public synchronized void schedule(long when) {
+        mAlarmManager.setExact(
+                AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler);
+        mScheduled = true;
+    }
+
+    /**
+     * Cancel all pending messages. This includes alarms that may have been fired, but have not been
+     * run on the handler yet.
+     */
+    public synchronized void cancel() {
+        if (mScheduled) {
+            mAlarmManager.cancel(this);
+            mScheduled = false;
+        }
+    }
+
+    @Override
+    public void onAlarm() {
+        // Once this method is called the alarm has already been fired and removed from
+        // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now
+        // be marked as unscheduled so that it can be rescheduled in the message handler.
+        final boolean stillScheduled;
+        synchronized (this) {
+            stillScheduled = mScheduled;
+            mScheduled = false;
+        }
+        if (stillScheduled) {
+            Message msg;
+            if (mRunnable == null) {
+                msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj);
+            } else {
+                msg = Message.obtain(mHandler, mRunnable);
+            }
+            mHandler.dispatchMessage(msg);
+            msg.recycle();
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
index 3630727..6ec39d9 100644
--- a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
+++ b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
@@ -1,5 +1,2 @@
-rule android.util.IndentingPrintWriter android.net.vcn.module.repackaged.android.util.IndentingPrintWriter
 rule android.util.LocalLog android.net.vcn.module.repackaged.android.util.LocalLog
-rule com.android.internal.util.IndentingPrintWriter android.net.vcn.module.repackaged.com.android.internal.util.IndentingPrintWriter
-rule com.android.internal.util.MessageUtils android.net.vcn.module.repackaged.com.android.internal.util.MessageUtils
 rule com.android.internal.util.WakeupMessage android.net.vcn.module.repackaged.com.android.internal.util.WakeupMessage
\ No newline at end of file
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index aeb2f5e..40726b4 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -767,7 +767,7 @@
                     params,
                     /* activityListener= */ null,
                     /* soundEffectListener= */ null);
-            return new VirtualDeviceManager.VirtualDevice(mImpl, getContext(), virtualDevice);
+            return new VirtualDeviceManager.VirtualDevice(getContext(), virtualDevice);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 31f6ef9..1d914c8 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -729,8 +729,10 @@
                 private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) {
                     pw.println("--- Module Details ---");
                     pw.println("Module name: " + moduleInfo.getName());
-                    pw.println("Module visibility: "
-                            + (moduleInfo.isHidden() ? "hidden" : "visible"));
+                    if (!android.content.pm.Flags.removeHiddenModuleUsage()) {
+                        pw.println("Module visibility: "
+                        + (moduleInfo.isHidden() ? "hidden" : "visible"));
+                    }
                 }
 
                 private void printAppDetails(PackageInfo packageInfo,
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3f6484f..00d23cc 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -7214,7 +7214,7 @@
         final int pid = Binder.getCallingPid();
         final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on)
                 .append(") from u/pid:").append(uid).append("/")
-                .append(pid).toString();
+                .append(pid).append(" src:AudioService.setBtA2dpOn").toString();
 
         new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
                 + MediaMetrics.SEPARATOR + "setBluetoothA2dpOn")
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 5f7ad27..db6d772 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -18,6 +18,8 @@
 
 import static android.content.pm.PackageManager.FEATURE_LEANBACK;
 import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.os.UserManager.isVisibleBackgroundUsersEnabled;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE;
 
 import static com.android.hardware.input.Flags.enableNew25q2Keycodes;
@@ -55,7 +57,6 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.view.Display;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
@@ -64,6 +65,8 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.IShortcutService;
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.policy.KeyCombinationManager;
 
 import java.util.ArrayDeque;
@@ -159,6 +162,10 @@
     /** Currently fully consumed key codes per device */
     private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>();
 
+    private final UserManagerInternal mUserManagerInternal;
+
+    private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled();
+
     KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) {
         mContext = context;
         mHandler = new Handler(looper, this::handleMessage);
@@ -180,6 +187,7 @@
         mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
         mInputGestureManager = new InputGestureManager(mContext);
         mInputDataStore = inputDataStore;
+        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
         initBehaviors();
         initKeyCombinationRules();
     }
@@ -449,6 +457,9 @@
     }
 
     public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+        if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
+            return false;
+        }
         final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
         if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
                 && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
@@ -457,6 +468,24 @@
         return false;
     }
 
+    private boolean shouldIgnoreKeyEventForVisibleBackgroundUser(KeyEvent event) {
+        final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(
+                event.getDisplayId());
+        final int currentUserId;
+        synchronized (mUserLock) {
+            currentUserId = mCurrentUserId;
+        }
+        if (currentUserId != displayAssignedUserId
+                && !KeyEvent.isVisibleBackgroundUserAllowedKey(event.getKeyCode())) {
+            if (DEBUG) {
+                Slog.w(TAG, "Ignored key event [" + event + "] for visible background user ["
+                        + displayAssignedUserId + "]");
+            }
+            return true;
+        }
+        return false;
+    }
+
     public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
             int policyFlags) {
         // TODO(b/358569822): Handle shortcuts trigger logic here and pass it to appropriate
@@ -895,7 +924,7 @@
     private void handleMultiKeyGesture(int[] keycodes,
             @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) {
         handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0,
-                gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags,
+                gestureType, action, DEFAULT_DISPLAY, /* focusedToken = */null, flags,
                 /* appLaunchData = */null);
     }
 
@@ -903,7 +932,7 @@
             @Nullable AppLaunchData appLaunchData) {
         handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, new int[0], /* modifierState= */0,
                 keyGestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE,
-                Display.DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData);
+                DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData);
     }
 
     @VisibleForTesting
@@ -915,6 +944,11 @@
     }
 
     private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) {
+        if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY
+                && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType,
+                event.displayId)) {
+            return false;
+        }
         synchronized (mKeyGestureHandlerRecords) {
             for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
                 if (handler.handleKeyGesture(event, focusedToken)) {
@@ -927,6 +961,24 @@
         return false;
     }
 
+    private boolean shouldIgnoreGestureEventForVisibleBackgroundUser(
+            @KeyGestureEvent.KeyGestureType int gestureType, int displayId) {
+        final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(displayId);
+        final int currentUserId;
+        synchronized (mUserLock) {
+            currentUserId = mCurrentUserId;
+        }
+        if (currentUserId != displayAssignedUserId
+                && !KeyGestureEvent.isVisibleBackgrounduserAllowedGesture(gestureType)) {
+            if (DEBUG) {
+                Slog.w(TAG, "Ignored gesture event [" + gestureType
+                        + "] for visible background user [" + displayAssignedUserId + "]");
+            }
+            return true;
+        }
+        return false;
+    }
+
     private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
         synchronized (mKeyGestureHandlerRecords) {
             for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
@@ -943,7 +995,7 @@
         // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally
         //  should not rely on PWM to tell us about the gesture start and end.
         AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState,
-                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY,
+                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY,
                 /* flags = */0, /* appLaunchData = */null);
         mHandler.obtainMessage(MSG_NOTIFY_KEY_GESTURE_EVENT, event).sendToTarget();
     }
@@ -951,7 +1003,7 @@
     public void handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
             @KeyGestureEvent.KeyGestureType int gestureType) {
         AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState,
-                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY,
+                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY,
                 /* flags = */0, /* appLaunchData = */null);
         handleKeyGesture(event, null /*focusedToken*/);
     }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index f5ed4d5..3018cfd 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -420,6 +420,25 @@
     }
 
     /* package */ void onMessageReceived(int sessionId, HubMessage message) {
+        byte code = onMessageReceivedInternal(sessionId, message);
+        if (code != ErrorCode.OK && message.isResponseRequired()) {
+            sendMessageDeliveryStatus(
+                    sessionId, message.getMessageSequenceNumber(), code);
+        }
+    }
+
+    /* package */ void onMessageDeliveryStatusReceived(
+            int sessionId, int sequenceNumber, byte errorCode) {
+        mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
+    }
+
+    /* package */ boolean hasSessionId(int sessionId) {
+        synchronized (mOpenSessionLock) {
+            return mSessionInfoMap.contains(sessionId);
+        }
+    }
+
+    private byte onMessageReceivedInternal(int sessionId, HubMessage message) {
         HubEndpointInfo remote;
         synchronized (mOpenSessionLock) {
             if (!isSessionActive(sessionId)) {
@@ -429,9 +448,7 @@
                                 + sessionId
                                 + ") with message: "
                                 + message);
-                sendMessageDeliveryStatus(
-                        sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMANENT_ERROR);
-                return;
+                return ErrorCode.PERMANENT_ERROR;
             }
             remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo();
         }
@@ -453,28 +470,12 @@
                             + ". "
                             + mPackageName
                             + " doesn't have permission");
-            sendMessageDeliveryStatus(
-                    sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMISSION_DENIED);
-            return;
+            return ErrorCode.PERMISSION_DENIED;
         }
 
         boolean success =
                 invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message));
-        if (!success) {
-            sendMessageDeliveryStatus(
-                    sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR);
-        }
-    }
-
-    /* package */ void onMessageDeliveryStatusReceived(
-            int sessionId, int sequenceNumber, byte errorCode) {
-        mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
-    }
-
-    /* package */ boolean hasSessionId(int sessionId) {
-        synchronized (mOpenSessionLock) {
-            return mSessionInfoMap.contains(sessionId);
-        }
+        return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR;
     }
 
     /**
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 286238e..0d0cdd8 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -438,9 +438,9 @@
         }
         LockscreenCredential credential =
                 LockscreenCredential.createUnifiedProfilePassword(newPassword);
-        Arrays.fill(newPasswordChars, '\u0000');
-        Arrays.fill(newPassword, (byte) 0);
-        Arrays.fill(randomLockSeed, (byte) 0);
+        LockPatternUtils.zeroize(newPasswordChars);
+        LockPatternUtils.zeroize(newPassword);
+        LockPatternUtils.zeroize(randomLockSeed);
         return credential;
     }
 
@@ -1537,7 +1537,7 @@
                         + userId);
             }
         } finally {
-            Arrays.fill(password, (byte) 0);
+            LockPatternUtils.zeroize(password);
         }
     }
 
@@ -1570,7 +1570,7 @@
         decryptionResult = cipher.doFinal(encryptedPassword);
         LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword(
                 decryptionResult);
-        Arrays.fill(decryptionResult, (byte) 0);
+        LockPatternUtils.zeroize(decryptionResult);
         try {
             long parentSid = getGateKeeperService().getSecureUserId(
                     mUserManager.getProfileParent(userId).id);
@@ -2263,7 +2263,7 @@
         } catch (RemoteException e) {
             Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId);
         } finally {
-            Arrays.fill(secret, (byte) 0);
+            LockPatternUtils.zeroize(secret);
         }
     }
 
diff --git a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
index 21caf76..3d64f18 100644
--- a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
+++ b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
@@ -26,6 +26,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.LockscreenCredential;
 
 import java.security.GeneralSecurityException;
@@ -154,7 +155,7 @@
             }
             LockscreenCredential result =
                     LockscreenCredential.createUnifiedProfilePassword(credential);
-            Arrays.fill(credential, (byte) 0);
+            LockPatternUtils.zeroize(credential);
             return result;
         }
     }
@@ -175,7 +176,7 @@
                 Slog.d(TAG, "Cannot delete key", e);
             }
             if (mEncryptedPasswords.contains(userId)) {
-                Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0);
+                LockPatternUtils.zeroize(mEncryptedPasswords.get(userId));
                 mEncryptedPasswords.remove(userId);
             }
         }
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
index bf1b3c3..85dc811 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
@@ -162,7 +162,7 @@
             Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
         } finally {
             if (mCredential != null) {
-                Arrays.fill(mCredential, (byte) 0); // no longer needed.
+                LockPatternUtils.zeroize(mCredential); // no longer needed.
             }
         }
     }
@@ -506,7 +506,7 @@
 
         try {
             byte[] hash = MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
-            Arrays.fill(bytes, (byte) 0);
+            LockPatternUtils.zeroize(bytes);
             return hash;
         } catch (NoSuchAlgorithmException e) {
             // Impossible, SHA-256 must be supported on Android.
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index 54303c0..7d8300a 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -1082,7 +1082,7 @@
             int keyguardCredentialsType = lockPatternUtilsToKeyguardType(savedCredentialType);
             try (LockscreenCredential credential =
                     createLockscreenCredential(keyguardCredentialsType, decryptedCredentials)) {
-                Arrays.fill(decryptedCredentials, (byte) 0);
+                LockPatternUtils.zeroize(decryptedCredentials);
                 decryptedCredentials = null;
                 VerifyCredentialResponse verifyResponse =
                         lockSettingsService.verifyCredential(credential, userId, 0);
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
index 0e66746..f1ef333 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
@@ -19,8 +19,9 @@
 import android.annotation.Nullable;
 import android.util.SparseArray;
 
+import com.android.internal.widget.LockPatternUtils;
+
 import java.util.ArrayList;
-import java.util.Arrays;
 
 import javax.security.auth.Destroyable;
 
@@ -187,8 +188,8 @@
          */
         @Override
         public void destroy() {
-            Arrays.fill(mLskfHash, (byte) 0);
-            Arrays.fill(mKeyClaimant, (byte) 0);
+            LockPatternUtils.zeroize(mLskfHash);
+            LockPatternUtils.zeroize(mKeyClaimant);
         }
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 68e195d..35bb199 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -302,7 +302,9 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            mAudioService.setBluetoothA2dpOn(on);
+            if (!Flags.disableSetBluetoothAd2pOnCalls()) {
+                mAudioService.setBluetoothA2dpOn(on);
+            }
         } catch (RemoteException ex) {
             Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn. on=" + on);
         } finally {
@@ -677,7 +679,9 @@
                 if (DEBUG) {
                     Slog.d(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")");
                 }
-                mAudioService.setBluetoothA2dpOn(a2dpOn);
+                if (!Flags.disableSetBluetoothAd2pOnCalls()) {
+                    mAudioService.setBluetoothA2dpOn(a2dpOn);
+                }
             }
         } catch (RemoteException e) {
             Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn.");
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 86fc732..d440d3a 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -21,6 +21,7 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.hardware.tv.mediaquality.IMediaQuality;
 import android.media.quality.AmbientBacklightSettings;
 import android.media.quality.IAmbientBacklightCallback;
 import android.media.quality.IMediaQualityManager;
@@ -35,9 +36,11 @@
 import android.media.quality.SoundProfileHandle;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.PersistableBundle;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.Pair;
@@ -45,6 +48,7 @@
 import android.util.SparseArray;
 
 import com.android.server.SystemService;
+import com.android.server.utils.Slogf;
 
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -74,6 +78,7 @@
     private final BiMap<Long, String> mSoundProfileTempIdMap;
     private final PackageManager mPackageManager;
     private final SparseArray<UserState> mUserStates = new SparseArray<>();
+    private IMediaQuality mMediaQuality;
 
     public MediaQualityService(Context context) {
         super(context);
@@ -88,6 +93,12 @@
 
     @Override
     public void onStart() {
+        IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default");
+        if (binder != null) {
+            Slogf.d(TAG, "binder is not null");
+            mMediaQuality = IMediaQuality.Stub.asInterface(binder);
+        }
+
         publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
     }
 
@@ -809,10 +820,29 @@
             if (!hasGlobalPictureQualityServicePermission()) {
                 //TODO: error handling
             }
+
+            try {
+                if (mMediaQuality != null) {
+                    mMediaQuality.setAutoPqEnabled(enabled);
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set auto picture quality", e);
+            }
         }
 
         @Override
         public boolean isAutoPictureQualityEnabled(UserHandle user) {
+            try {
+                if (mMediaQuality != null) {
+                    return mMediaQuality.getAutoPqEnabled();
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to get auto picture quality", e);
+            }
             return false;
         }
 
@@ -821,10 +851,29 @@
             if (!hasGlobalPictureQualityServicePermission()) {
                 //TODO: error handling
             }
+
+            try {
+                if (mMediaQuality != null) {
+                    mMediaQuality.setAutoSrEnabled(enabled);
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set auto super resolution", e);
+            }
         }
 
         @Override
         public boolean isSuperResolutionEnabled(UserHandle user) {
+            try {
+                if (mMediaQuality != null) {
+                    return mMediaQuality.getAutoSrEnabled();
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to get auto super resolution", e);
+            }
             return false;
         }
 
@@ -833,10 +882,29 @@
             if (!hasGlobalSoundQualityServicePermission()) {
                 //TODO: error handling
             }
+
+            try {
+                if (mMediaQuality != null) {
+                    mMediaQuality.setAutoAqEnabled(enabled);
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set auto audio quality", e);
+            }
         }
 
         @Override
         public boolean isAutoSoundQualityEnabled(UserHandle user) {
+            try {
+                if (mMediaQuality != null) {
+                    return mMediaQuality.getAutoAqEnabled();
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to get auto audio quality", e);
+            }
             return false;
         }
 
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 4b41696..e47f8ae9 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -583,6 +583,15 @@
         final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(
                 record.getUserId(), pkgName, sectioner);
 
+        // The notification was part of a different section => trigger regrouping
+        final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record);
+        if (prevSectionKey != null && !fullAggregateGroupKey.equals(prevSectionKey)) {
+            if (DEBUG) {
+                Slog.i(TAG, "Section changed for: " + record);
+            }
+            maybeUngroupOnSectionChanged(record, prevSectionKey);
+        }
+
         // This notification is already aggregated
         if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) {
             return false;
@@ -652,10 +661,33 @@
     }
 
     /**
+     * A notification was added that was previously part of a different section and needs to trigger
+     * GH state cleanup.
+     */
+    private void maybeUngroupOnSectionChanged(NotificationRecord record,
+            FullyQualifiedGroupKey prevSectionKey) {
+        maybeUngroupWithSections(record, prevSectionKey);
+        if (record.getGroupKey().equals(prevSectionKey.toString())) {
+            record.setOverrideGroupKey(null);
+        }
+    }
+
+    /**
      * A notification was added that is app-grouped.
      */
     private void maybeUngroupOnAppGrouped(NotificationRecord record) {
-        maybeUngroupWithSections(record, getSectionGroupKeyWithFallback(record));
+        FullyQualifiedGroupKey currentSectionKey = getSectionGroupKeyWithFallback(record);
+
+        // The notification was part of a different section => trigger regrouping
+        final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record);
+        if (prevSectionKey != null && !prevSectionKey.equals(currentSectionKey)) {
+            if (DEBUG) {
+                Slog.i(TAG, "Section changed for: " + record);
+            }
+            currentSectionKey = prevSectionKey;
+        }
+
+        maybeUngroupWithSections(record, currentSectionKey);
     }
 
     /**
diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java
index 7cbbe29..5a42505 100644
--- a/services/core/java/com/android/server/notification/NotificationDelegate.java
+++ b/services/core/java/com/android/server/notification/NotificationDelegate.java
@@ -107,4 +107,9 @@
      * @param key the notification key
      */
     void unbundleNotification(String key);
+    /**
+     *  Called when the notification should be rebundled.
+     * @param key the notification key
+     */
+    void rebundleNotification(String key);
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index dd9741c..341038f 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1888,6 +1888,36 @@
                 }
             }
         }
+
+        @Override
+        public void rebundleNotification(String key) {
+            if (!(notificationClassification() && notificationRegroupOnClassification())) {
+                return;
+            }
+            synchronized (mNotificationLock) {
+                NotificationRecord r = mNotificationsByKey.get(key);
+                if (r == null) {
+                    return;
+                }
+
+                if (DBG) {
+                    Slog.v(TAG, "rebundleNotification: " + r);
+                }
+
+                if (r.getBundleType() != Adjustment.TYPE_OTHER) {
+                    final Bundle classifBundle = new Bundle();
+                    classifBundle.putInt(KEY_TYPE, r.getBundleType());
+                    Adjustment adj = new Adjustment(r.getSbn().getPackageName(), r.getKey(),
+                            classifBundle, "rebundle", r.getUserId());
+                    applyAdjustmentLocked(r, adj, /* isPosted= */ true);
+                    mRankingHandler.requestSort();
+                } else {
+                    if (DBG) {
+                        Slog.w(TAG, "Can't rebundle. No valid bundle type for: " + r);
+                    }
+                }
+            }
+        }
     };
 
     NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() {
@@ -7134,6 +7164,7 @@
                     adjustments.putParcelable(KEY_TYPE, newChannel);
 
                     logClassificationChannelAdjustmentReceived(r, isPosted, classification);
+                    r.setBundleType(classification);
                 }
             }
             r.addAdjustment(adjustment);
@@ -9537,7 +9568,8 @@
                                     || !Objects.equals(oldSbn.getNotification().getGroup(),
                                         n.getNotification().getGroup())
                                     || oldSbn.getNotification().flags
-                                    != n.getNotification().flags) {
+                                    != n.getNotification().flags
+                                    || !old.getChannel().getId().equals(r.getChannel().getId())) {
                                 synchronized (mNotificationLock) {
                                     final String autogroupName =
                                             notificationForceGrouping() ?
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 0bb3c6a..81af0d8 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -222,6 +222,9 @@
     // lifetime extended.
     private boolean mCanceledAfterLifetimeExtension = false;
 
+    // type of the bundle if the notification was classified
+    private @Adjustment.Types int mBundleType = Adjustment.TYPE_OTHER;
+
     public NotificationRecord(Context context, StatusBarNotification sbn,
             NotificationChannel channel) {
         this.sbn = sbn;
@@ -467,6 +470,10 @@
             }
         }
 
+        if (android.service.notification.Flags.notificationClassification()) {
+            mBundleType = previous.mBundleType;
+        }
+
         // Don't copy importance information or mGlobalSortKey, recompute them.
     }
 
@@ -1629,6 +1636,14 @@
         mCanceledAfterLifetimeExtension = canceledAfterLifetimeExtension;
     }
 
+    public @Adjustment.Types int getBundleType() {
+        return mBundleType;
+    }
+
+    public void setBundleType(@Adjustment.Types int bundleType) {
+        mBundleType = bundleType;
+    }
+
     /**
      * Whether this notification is a conversation notification.
      */
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 4c70d23..4cb7769 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -3060,6 +3060,8 @@
         }
 
         if (succeeded) {
+            Slog.i(TAG, "installation completed:" + packageName);
+
             if (Flags.aslInApkAppMetadataSource()
                     && pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) {
                 if (!extractAppMetadataFromApk(request.getPkg(),
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index a6f2a37..1cf24fc 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -20,7 +20,6 @@
 
 import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.power.hint.Flags.adpfSessionTag;
-import static com.android.server.power.hint.Flags.cpuHeadroomAffinityCheck;
 import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
 import static com.android.server.power.hint.Flags.resetOnForkEnabled;
 
@@ -1604,8 +1603,7 @@
                         }
                     }
                 }
-                if (cpuHeadroomAffinityCheck() && mCheckHeadroomAffinity
-                        && params.tids.length > 1) {
+                if (mCheckHeadroomAffinity && params.tids.length > 1) {
                     checkThreadAffinityForTids(params.tids);
                 }
                 halParams.tids = params.tids;
diff --git a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
index 1773971..a75d110 100644
--- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
+++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java
@@ -88,5 +88,6 @@
         out.println("    Print this help text.");
         out.println("  dump <PROCESS>");
         out.println("    Dump the Resources objects in use as well as the history of Resources");
+
     }
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 4ed5f90..a19a342 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -2199,6 +2199,19 @@
         });
     }
 
+    /**
+     *  Called when the notification should be rebundled.
+     * @param key the notification key
+     */
+    @Override
+    public void rebundleNotification(String key) {
+        enforceStatusBarService();
+        enforceValidCallingUser();
+        Binder.withCleanCallingIdentity(() -> {
+            mNotificationDelegate.rebundleNotification(key);
+        });
+    }
+
 
     @Override
     public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 3d53078..1fe6159 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -232,6 +232,7 @@
 import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT;
 import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE;
 import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
@@ -2814,9 +2815,27 @@
         attachStartingSurfaceToAssociatedTask();
     }
 
+    /**
+     * If the device is locked and the app does not request showWhenLocked,
+     * defer removing the starting window until the transition is complete.
+     * This prevents briefly appearing the app context and causing secure concern.
+     */
+    void deferStartingWindowRemovalForKeyguardUnoccluding() {
+        if (mStartingData.mRemoveAfterTransaction != AFTER_TRANSITION_FINISH
+                && isKeyguardLocked() && !canShowWhenLockedInner(this) && !isVisibleRequested()
+                && mTransitionController.inTransition(this)) {
+            mStartingData.mRemoveAfterTransaction = AFTER_TRANSITION_FINISH;
+        }
+    }
+
     void removeStartingWindow() {
         boolean prevEligibleForLetterboxEducation = isEligibleForLetterboxEducation();
 
+        if (mStartingData != null
+                && mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) {
+            return;
+        }
+
         if (transferSplashScreenIfNeeded()) {
             return;
         }
@@ -4655,6 +4674,9 @@
                 tStartingWindow.mToken = this;
                 tStartingWindow.mActivityRecord = this;
 
+                if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) {
+                    mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE;
+                }
                 if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) {
                     // The removal of starting window should wait for window drawn of current
                     // activity.
@@ -8125,10 +8147,7 @@
         if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) {
             // We use Task here because we want to be consistent with what happens in
             // multi-window mode where other tasks orientations are ignored.
-            final ActivityRecord belowCandidate = task.getActivity(
-                    a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
-                    this /* boundary */, false /* includeBoundary */,
-                    true /* traverseTopToBottom */);
+            final ActivityRecord belowCandidate = task.getActivityBelowForDefiningOrientation(this);
             if (belowCandidate != null) {
                 return belowCandidate.getRequestedConfigurationOrientation(forDisplay);
             }
diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
index d49a507..5bec442 100644
--- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
@@ -25,6 +25,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
 import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
 import static android.window.DisplayAreaOrganizer.FEATURE_FULLSCREEN_MAGNIFICATION;
 import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT;
@@ -151,6 +153,12 @@
                                 .all()
                                 .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
                                         TYPE_SECURE_SYSTEM_OVERLAY)
+                                .build())
+                        .addFeature(new Feature.Builder(wmService.mPolicy, "AppZoomOut",
+                                FEATURE_APP_ZOOM_OUT)
+                                .all()
+                                .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
+                                        TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE, TYPE_WALLPAPER)
                                 .build());
             }
             rootHierarchy
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index dd23f57..acd47da 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1822,7 +1822,13 @@
      */
     private void applyFixedRotationForNonTopVisibleActivityIfNeeded(@NonNull ActivityRecord ar,
             @ActivityInfo.ScreenOrientation int topOrientation) {
-        final int orientation = ar.getRequestedOrientation();
+        int orientation = ar.getRequestedOrientation();
+        if (orientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
+            final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(ar);
+            if (nextCandidate != null) {
+                orientation = nextCandidate.getRequestedOrientation();
+            }
+        }
         if (orientation == topOrientation || ar.inMultiWindowMode()
                 || ar.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) {
             return;
@@ -1864,9 +1870,7 @@
             return ROTATION_UNDEFINED;
         }
         if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
-            final ActivityRecord nextCandidate = getActivity(
-                    a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
-                    r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
+            final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(r);
             if (nextCandidate != null) {
                 r = nextCandidate;
                 activityOrientation = r.getOverrideOrientation();
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 3a0e41a..b3e9244 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -45,6 +45,7 @@
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Build;
@@ -70,6 +71,7 @@
 import com.android.internal.view.IDragAndDropPermissions;
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
+import com.android.window.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.concurrent.CompletableFuture;
@@ -542,10 +544,26 @@
                 }
             }
             ClipDescription description = data != null ? data.getDescription() : mDataDescription;
+
+            // Note this can be negative numbers if touch coords are left or top of the window.
+            PointF relativeToWindowCoords = new PointF(newWin.translateToWindowX(touchX),
+                    newWin.translateToWindowY(touchY));
+            if (Flags.enableConnectedDisplaysDnd()
+                    && mDisplayContent.getDisplayId() != newWin.getDisplayId()) {
+                // Currently DRAG_STARTED coords are sent relative to the window target in **px**
+                // coordinates. However, this cannot be extended to connected displays scenario,
+                // as there's only global **dp** coordinates and no global **px** coordinates.
+                // Hence, the coords sent here will only try to indicate that drag started outside
+                // this window display, but relative distance should not be calculated or depended
+                // on.
+                relativeToWindowCoords = new PointF(-newWin.getBounds().left - 1,
+                        -newWin.getBounds().top - 1);
+            }
+
             DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED,
-                    newWin.translateToWindowX(touchX), newWin.translateToWindowY(touchY),
-                    description, data, false /* includeDragSurface */,
-                    true /* includeDragFlags */, null /* dragAndDropPermission */);
+                    relativeToWindowCoords.x, relativeToWindowCoords.y, description, data,
+                    false /* includeDragSurface */, true /* includeDragFlags */,
+                    null /* dragAndDropPermission */);
             try {
                 newWin.mClient.dispatchDragEvent(event);
                 // track each window that we've notified that the drag is starting
diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java
index 9dc3d6a..bc16a56 100644
--- a/services/core/java/com/android/server/wm/PersisterQueue.java
+++ b/services/core/java/com/android/server/wm/PersisterQueue.java
@@ -86,6 +86,34 @@
         mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread");
     }
 
+    /**
+     * Busy wait until {@link #mLazyTaskWriterThread} is in {@link Thread.State#WAITING}, or
+     * times out. This indicates the thread is waiting for new tasks to appear. If the wait
+     * succeeds, this queue waits at least {@link #mPreTaskDelayMs} milliseconds before running the
+     * next task.
+     *
+     * <p>This is for testing purposes only.
+     *
+     * @param timeoutMillis the maximum time of waiting in milliseconds
+     * @return {@code true} if the thread is in {@link Thread.State#WAITING} at return
+     */
+    @VisibleForTesting
+    boolean waitUntilWritingThreadIsWaiting(long timeoutMillis) {
+        final long timeoutTime = SystemClock.uptimeMillis() + timeoutMillis;
+        do {
+            Thread.State state;
+            synchronized (this) {
+                state = mLazyTaskWriterThread.getState();
+            }
+            if (state == Thread.State.WAITING) {
+                return true;
+            }
+            Thread.yield();
+        } while (SystemClock.uptimeMillis() < timeoutTime);
+
+        return false;
+    }
+
     synchronized void startPersisting() {
         if (!mLazyTaskWriterThread.isAlive()) {
             mLazyTaskWriterThread.start();
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
index a545454..3eb13c5 100644
--- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -407,10 +407,8 @@
             bitmap.recycle();
 
             final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId);
-            try {
-                FileOutputStream fos = new FileOutputStream(file);
+            try (FileOutputStream fos = new FileOutputStream(file)) {
                 swBitmap.compress(JPEG, COMPRESS_QUALITY, fos);
-                fos.close();
             } catch (IOException e) {
                 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
                 return false;
@@ -428,10 +426,8 @@
             swBitmap.recycle();
 
             final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId);
-            try {
-                FileOutputStream lowResFos = new FileOutputStream(lowResFile);
+            try (FileOutputStream lowResFos = new FileOutputStream(lowResFile)) {
                 lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos);
-                lowResFos.close();
             } catch (IOException e) {
                 Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
                 return false;
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index 7349224..1a7a619 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -31,11 +31,18 @@
     static final int AFTER_TRANSACTION_REMOVE_DIRECTLY = 1;
     /** Do copy splash screen to client after transaction done. */
     static final int AFTER_TRANSACTION_COPY_TO_CLIENT = 2;
+    /**
+     * Remove the starting window after transition finish.
+     * Used when activity doesn't request show when locked, so the app window should never show to
+     * the user if device is locked.
+     **/
+    static final int AFTER_TRANSITION_FINISH = 3;
 
     @IntDef(prefix = { "AFTER_TRANSACTION" }, value = {
             AFTER_TRANSACTION_IDLE,
             AFTER_TRANSACTION_REMOVE_DIRECTLY,
             AFTER_TRANSACTION_COPY_TO_CLIENT,
+            AFTER_TRANSITION_FINISH,
     })
     @interface AfterTransaction {}
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d92301b..9c1cf6e 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5255,6 +5255,10 @@
             return false;
         }
 
+        if (!mTaskSupervisor.readyToResume()) {
+            return false;
+        }
+
         final ActivityRecord topActivity = topRunningActivity(true /* focusableOnly */);
         if (topActivity == null) {
             // There are no activities left in this task, let's look somewhere else.
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index a964678..f4a455a 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -70,6 +70,8 @@
 import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM;
 import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
 import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
+import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION;
@@ -1374,6 +1376,13 @@
                         enterAutoPip = true;
                     }
                 }
+
+                if (ar.mStartingData != null && ar.mStartingData.mRemoveAfterTransaction
+                        == AFTER_TRANSITION_FINISH
+                        && (!ar.isVisible() || !ar.mTransitionController.inTransition(ar))) {
+                    ar.mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE;
+                    ar.removeStartingWindow();
+                }
                 final ChangeInfo changeInfo = mChanges.get(ar);
                 // Due to transient-hide, there may be some activities here which weren't in the
                 // transition.
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index aa60f93..54a3d41 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1659,6 +1659,12 @@
         return ORIENTATION_UNDEFINED;
     }
 
+    @Nullable
+    ActivityRecord getActivityBelowForDefiningOrientation(ActivityRecord from) {
+        return getActivity(ActivityRecord::canDefineOrientationForActivitiesAbove,
+                from /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
+    }
+
     /**
      * Calls {@link #setOrientation(int, WindowContainer)} with {@code null} to the last 2
      * parameters.
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 3a2a1ea..d69b06a 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -126,6 +126,7 @@
 import static com.android.server.wm.MoveAnimationSpecProto.DURATION_MS;
 import static com.android.server.wm.MoveAnimationSpecProto.FROM;
 import static com.android.server.wm.MoveAnimationSpecProto.TO;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
@@ -1920,6 +1921,13 @@
         }
         final ActivityRecord atoken = mActivityRecord;
         if (atoken != null) {
+            if (atoken.mStartingData != null && mAttrs.type != TYPE_APPLICATION_STARTING
+                    && atoken.mStartingData.mRemoveAfterTransaction
+                    == AFTER_TRANSITION_FINISH) {
+                // Preventing app window from visible during un-occluding animation playing due to
+                // alpha blending.
+                return false;
+            }
             final boolean isVisible = isStartingWindowAssociatedToTask()
                     ? mStartingData.mAssociatedTask.isVisible() : atoken.isVisible();
             return ((!isParentWindowHidden() && isVisible)
@@ -2925,7 +2933,14 @@
             final int mask = FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD
                     | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
             WindowManager.LayoutParams sa = mActivityRecord.mStartingWindow.mAttrs;
+            final boolean wasShowWhenLocked = (sa.flags & FLAG_SHOW_WHEN_LOCKED) != 0;
+            final boolean removeShowWhenLocked = (mAttrs.flags & FLAG_SHOW_WHEN_LOCKED) == 0;
             sa.flags = (sa.flags & ~mask) | (mAttrs.flags & mask);
+            if (Flags.keepAppWindowHideWhileLocked() && wasShowWhenLocked && removeShowWhenLocked) {
+                // Trigger unoccluding animation if needed.
+                mActivityRecord.checkKeyguardFlagsChanged();
+                mActivityRecord.deferStartingWindowRemovalForKeyguardUnoccluding();
+            }
         }
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e69a741..d2d3884 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -8909,11 +8909,15 @@
 
         if (parent) {
             Preconditions.checkCallAuthorization(
-                    isProfileOwnerOfOrganizationOwnedDevice(getCallerIdentity().getUserId()));
+                    isProfileOwnerOfOrganizationOwnedDevice(caller.getUserId()));
+            // If a DPC is querying on the parent instance, make sure it's only querying the parent
+            // user of itself. Querying any other user is not allowed.
+            Preconditions.checkArgument(caller.getUserId() == userHandle);
         }
+        int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle;
         Boolean disallowed = mDevicePolicyEngine.getResolvedPolicy(
                 PolicyDefinition.SCREEN_CAPTURE_DISABLED,
-                userHandle);
+                affectedUserId);
         return disallowed != null && disallowed;
     }
 
@@ -14669,7 +14673,7 @@
     @Override
     public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled,
             PersistableBundle options) {
-        if (Flags.secondaryLockscreenApiEnabled()) {
+        if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) {
             final CallerIdentity caller = getCallerIdentity();
             final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller);
             synchronized (getLockObject()) {
@@ -14680,16 +14684,8 @@
                         caller.getUserId());
             }
 
-            if (mSupervisionManagerInternal != null) {
-                mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser(
-                        caller.getUserId(), enabled, options);
-            } else {
-                synchronized (getLockObject()) {
-                    DevicePolicyData policy = getUserData(caller.getUserId());
-                    policy.mSecondaryLockscreenEnabled = enabled;
-                    saveSettingsLocked(caller.getUserId());
-                }
-            }
+            mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser(
+                    caller.getUserId(), enabled, options);
         } else {
             Objects.requireNonNull(who, "ComponentName is null");
 
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index c5d42ad..8e06ed8 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2714,16 +2714,18 @@
             mSystemServiceManager.startService(AuthService.class);
             t.traceEnd();
 
-            if (android.security.Flags.secureLockdown()) {
-                t.traceBegin("StartSecureLockDeviceService.Lifecycle");
-                mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class);
-                t.traceEnd();
-            }
+            if (!isWatch && !isTv && !isAutomotive) {
+                if (android.security.Flags.secureLockdown()) {
+                    t.traceBegin("StartSecureLockDeviceService.Lifecycle");
+                    mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class);
+                    t.traceEnd();
+                }
 
-            if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
-                t.traceBegin("StartAuthenticationPolicyService");
-                mSystemServiceManager.startService(AuthenticationPolicyService.class);
-                t.traceEnd();
+                if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
+                    t.traceBegin("StartAuthenticationPolicyService");
+                    mSystemServiceManager.startService(AuthenticationPolicyService.class);
+                    t.traceEnd();
+                }
             }
 
             if (!isWatch) {
diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index cd94c0f..e615712 100644
--- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -70,8 +70,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SessionCreationConfig;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -1388,7 +1386,6 @@
 
 
     @Test
-    @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
     public void testCpuHeadroomCache() throws Exception {
         CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal();
         CpuHeadroomParams halParams1 = new CpuHeadroomParams();
@@ -1476,8 +1473,7 @@
     }
 
     @Test
-    @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
-    public void testGetCpuHeadroomDifferentAffinity_flagOn() throws Exception {
+    public void testGetCpuHeadroomDifferentAffinity() throws Exception {
         CountDownLatch latch = new CountDownLatch(2);
         int[] tids = createThreads(2, latch);
         CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
@@ -1497,28 +1493,6 @@
         verify(mIPowerMock, times(0)).getCpuHeadroom(any());
     }
 
-    @Test
-    @DisableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
-    public void testGetCpuHeadroomDifferentAffinity_flagOff() throws Exception {
-        CountDownLatch latch = new CountDownLatch(2);
-        int[] tids = createThreads(2, latch);
-        CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
-        params.tids = tids;
-        CpuHeadroomParams halParams = new CpuHeadroomParams();
-        halParams.tids = tids;
-        float headroom = 0.1f;
-        CpuHeadroomResult halRet = CpuHeadroomResult.globalHeadroom(headroom);
-        String ret1 = runAndWaitForCommand("taskset -p 1 " + tids[0]);
-        String ret2 = runAndWaitForCommand("taskset -p 3 " + tids[1]);
-
-        HintManagerService service = createService();
-        clearInvocations(mIPowerMock);
-        when(mIPowerMock.getCpuHeadroom(eq(halParams))).thenReturn(halRet);
-        assertEquals("taskset cmd return: " + ret1 + "\n" + ret2, halRet,
-                service.getBinderServiceInstance().getCpuHeadroom(params));
-        verify(mIPowerMock, times(1)).getCpuHeadroom(any());
-    }
-
     private String runAndWaitForCommand(String command) throws Exception {
         java.lang.Process process = Runtime.getRuntime().exec(command);
         BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index d6ca10a..07b18db 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -27,6 +27,7 @@
         "servicestests-utils",
         "platform-test-annotations",
         "flag-junit",
+        "apct-perftests-utils",
     ],
 
     libs: [
@@ -64,10 +65,12 @@
         "ravenwood-junit",
         "truth",
         "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
         "androidx.test.rules",
         "androidx.test.uiautomator_uiautomator",
         "modules-utils-binary-xml",
         "flag-junit",
+        "apct-perftests-utils",
     ],
     srcs: [
         "src/com/android/server/power/stats/*.java",
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java
new file mode 100644
index 0000000..cc75e9e
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.power.stats;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.BatteryStatsManager;
+import android.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
+import android.os.ParcelFileDescriptor;
+import android.perftests.utils.TraceMarkParser;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.uiautomator.UiDevice;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@android.platform.test.annotations.DisabledOnRavenwood(reason = "Atrace event test")
+public class BatteryStatsHistoryTraceTest {
+    private static final String ATRACE_START = "atrace --async_start -b 1024 -c ss";
+    private static final String ATRACE_STOP = "atrace --async_stop";
+    private static final String ATRACE_DUMP = "atrace --async_dump";
+
+    @Before
+    public void before() throws Exception {
+        runShellCommand(ATRACE_START);
+    }
+
+    @After
+    public void after() throws Exception {
+        runShellCommand(ATRACE_STOP);
+    }
+
+    @Test
+    public void dumpsys() throws Exception {
+        runShellCommand("dumpsys batterystats --history");
+
+        Set<String> slices = readAtraceSlices();
+        assertThat(slices).contains("BatteryStatsHistory.copy");
+        assertThat(slices).contains("BatteryStatsHistory.iterate");
+    }
+
+    @Test
+    public void getBatteryUsageStats() throws Exception {
+        BatteryStatsManager batteryStatsManager =
+                getInstrumentation().getTargetContext().getSystemService(BatteryStatsManager.class);
+        BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
+                .includeBatteryHistory().build();
+        BatteryUsageStats batteryUsageStats = batteryStatsManager.getBatteryUsageStats(query);
+        assertThat(batteryUsageStats).isNotNull();
+
+        Set<String> slices = readAtraceSlices();
+        assertThat(slices).contains("BatteryStatsHistory.copy");
+        assertThat(slices).contains("BatteryStatsHistory.iterate");
+        assertThat(slices).contains("BatteryStatsHistory.writeToParcel");
+    }
+
+    private String runShellCommand(String cmd) throws Exception {
+        return UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd);
+    }
+
+    private Set<String> readAtraceSlices() throws Exception {
+        Set<String> keys = new HashSet<>();
+
+        TraceMarkParser parser = new TraceMarkParser(
+                line -> line.name.startsWith("BatteryStatsHistory."));
+        ParcelFileDescriptor pfd =
+                getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_DUMP);
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(pfd)))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                parser.visit(line);
+            }
+        }
+        parser.forAllSlices((key, slices) -> keys.add(key));
+        return keys;
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 6cb2429..fa733e8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -2509,6 +2509,134 @@
     }
 
     @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+    public void testRepostWithNewChannel_afterAutogrouping_isRegrouped() {
+        final String pkg = "package";
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        // Post ungrouped notifications => will be autogrouped
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord notification = getNotificationRecord(pkg, i + 42,
+                    String.valueOf(i + 42), UserHandle.SYSTEM, null, false);
+            notificationList.add(notification);
+            mGroupHelper.onNotificationPosted(notification, false);
+        }
+
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+                eq(expectedGroupKey), eq(true));
+
+        // Post ungrouped notifications to a different section, below autogroup limit
+        Mockito.reset(mCallback);
+        // Post ungrouped notifications => will be autogrouped
+        final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1",
+                "TEST_CHANNEL_ID1", IMPORTANCE_LOW);
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord notification = getNotificationRecord(pkg, i + 4242,
+                    String.valueOf(i + 4242), UserHandle.SYSTEM, null, false, silentChannel);
+            notificationList.add(notification);
+            mGroupHelper.onNotificationPosted(notification, false);
+        }
+
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
+        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+
+        // Update a notification to a different channel that moves it to a different section
+        Mockito.reset(mCallback);
+        final NotificationRecord notifToInvalidate = notificationList.get(0);
+        final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate);
+        final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2",
+                "TEST_CHANNEL_ID2", IMPORTANCE_LOW);
+        notifToInvalidate.updateNotificationChannel(updatedChannel);
+        assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection);
+        boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false);
+        assertThat(needsAutogrouping).isTrue();
+
+        // Check that the silent section was autogrouped
+        final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(silentSectionGroupKey), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+                eq(silentSectionGroupKey), eq(true));
+        verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+                eq(expectedGroupKey), any());
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+    public void testRepostWithNewChannel_afterForceGrouping_isRegrouped() {
+        final String pkg = "package";
+        final String groupName = "testGroup";
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post valid section summary notifications without children => force group
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord notification = getNotificationRecord(pkg, i + 42,
+                    String.valueOf(i + 42), UserHandle.SYSTEM, groupName, false);
+            notificationList.add(notification);
+            mGroupHelper.onNotificationPostedWithDelay(notification, notificationList,
+                    summaryByGroup);
+        }
+
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey), eq(true));
+
+        // Update a notification to a different channel that moves it to a different section
+        Mockito.reset(mCallback);
+        final NotificationRecord notifToInvalidate = notificationList.get(0);
+        final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate);
+        final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2",
+                "TEST_CHANNEL_ID2", IMPORTANCE_LOW);
+        notifToInvalidate.updateNotificationChannel(updatedChannel);
+        assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection);
+        boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false);
+
+        mGroupHelper.onNotificationPostedWithDelay(notifToInvalidate, notificationList,
+                summaryByGroup);
+
+        // Check that the updated notification is removed from the autogroup
+        assertThat(needsAutogrouping).isFalse();
+        verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+                eq(expectedGroupKey), any());
+
+        // Post child notifications for the silent sectin => will be autogrouped
+        Mockito.reset(mCallback);
+        final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1",
+                "TEST_CHANNEL_ID1", IMPORTANCE_LOW);
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord notification = getNotificationRecord(pkg, i + 4242,
+                    String.valueOf(i + 4242), UserHandle.SYSTEM, "aGroup", false, silentChannel);
+            notificationList.add(notification);
+            needsAutogrouping = mGroupHelper.onNotificationPosted(notification, false);
+            assertThat(needsAutogrouping).isFalse();
+            mGroupHelper.onNotificationPostedWithDelay(notification, notificationList,
+                    summaryByGroup);
+        }
+
+        // Check that the silent section was autogrouped
+        final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(silentSectionGroupKey), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(silentSectionGroupKey), eq(true));
+    }
+
+    @Test
     @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testMoveAggregateGroups_updateChannel() {
         final String pkg = "package";
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 7885c9b..e43b28b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -6341,6 +6341,26 @@
     }
 
     @Test
+    public void testOnlyAutogroupIfNeeded_channelChanged_ghUpdate() {
+        NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0,
+                "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false);
+        mService.addNotification(r);
+
+        NotificationRecord update = generateNotificationRecord(mSilentChannel, 0,
+                "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false);
+        mService.addEnqueuedNotification(update);
+
+        NotificationManagerService.PostNotificationRunnable runnable =
+                mService.new PostNotificationRunnable(update.getKey(),
+                        update.getSbn().getPackageName(), update.getUid(),
+                        mPostNotificationTrackerFactory.newTracker(null));
+        runnable.run();
+        waitForIdle();
+
+        verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean());
+    }
+
+    @Test
     public void testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate() {
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0,
                 "testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate", null, false);
@@ -17901,4 +17921,63 @@
         verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r1), eq(hasOriginalSummary));
     }
 
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION,
+            FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
+    public void testRebundleNotification_restoresBundleChannel() throws Exception {
+        NotificationManagerService.WorkerHandler handler = mock(
+                NotificationManagerService.WorkerHandler.class);
+        mService.setHandler(handler);
+        when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
+        when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true);
+        when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true);
+        when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString(), anyInt())).thenReturn(true);
+
+        // Post a single notification
+        final boolean hasOriginalSummary = false;
+        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+        final String keyToUnbundle = r.getKey();
+        mService.addNotification(r);
+
+        // Classify notification into the NEWS bundle
+        Bundle signals = new Bundle();
+        signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS);
+        Adjustment adjustment = new Adjustment(
+                r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier());
+        mBinderService.applyAdjustmentFromAssistant(null, adjustment);
+        waitForIdle();
+        r.applyAdjustments();
+        // Check that the NotificationRecord channel is updated
+        assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID);
+        assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS);
+
+        // Unbundle the notification
+        mService.mNotificationDelegate.unbundleNotification(keyToUnbundle);
+
+        // Check that the original channel was restored
+        assertThat(r.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID);
+        assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS);
+        verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r), eq(hasOriginalSummary));
+
+        Mockito.reset(mRankingHandler);
+        Mockito.reset(mGroupHelper);
+
+        // Rebundle the notification
+        mService.mNotificationDelegate.rebundleNotification(keyToUnbundle);
+
+        // Actually apply the adjustments
+        doAnswer(invocationOnMock -> {
+            ((NotificationRecord) invocationOnMock.getArguments()[0]).applyAdjustments();
+            ((NotificationRecord) invocationOnMock.getArguments()[0]).calculateImportance();
+            return null;
+        }).when(mRankingHelper).extractSignals(any(NotificationRecord.class));
+        mService.handleRankingSort();
+        verify(handler, times(1)).scheduleSendRankingUpdate();
+
+        // Check that the bundle channel was restored
+        verify(mRankingHandler, times(1)).requestSort();
+        assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID);
+    }
+
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 5486aa3..dfd10ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1183,6 +1183,18 @@
         assertEquals(prev, mDisplayContent.getLastOrientationSource());
         // The top will use the rotation from "prev" with fixed rotation.
         assertTrue(top.hasFixedRotationTransform());
+
+        mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp();
+        assertFalse(top.hasFixedRotationTransform());
+
+        // Assume that the requested orientation of "prev" is landscape. And the display is also
+        // rotated to landscape. The activities from bottom to top are TaskB{"prev, "behindTop"},
+        // TaskB{"top"}. Then "behindTop" should also get landscape according to ORIENTATION_BEHIND
+        // instead of resolving as undefined which causes to unexpected fixed portrait rotation.
+        final ActivityRecord behindTop = new ActivityBuilder(mAtm).setTask(prev.getTask())
+                .setOnTop(false).setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build();
+        mDisplayContent.applyFixedRotationForNonTopVisibleActivityIfNeeded(behindTop);
+        assertFalse(behindTop.hasFixedRotationTransform());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index de4b6fa..23dcb65 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -34,6 +34,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -46,12 +47,14 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.Intent;
 import android.content.pm.ShortcutServiceInternal;
 import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -60,6 +63,7 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.view.DragEvent;
 import android.view.InputChannel;
@@ -74,6 +78,7 @@
 
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
+import com.android.window.flags.Flags;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -141,17 +146,28 @@
         }
     }
 
+    private WindowState createDropTargetWindow(String name) {
+        return createDropTargetWindow(name, null /* targetDisplay */);
+    }
+
     /**
      * Creates a window state which can be used as a drop target.
      */
-    private WindowState createDropTargetWindow(String name, int ownerId) {
-        final Task task = new TaskBuilder(mSupervisor).setUserId(ownerId).build();
-        final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess(
-                mProcess).build();
+    private WindowState createDropTargetWindow(String name,
+            @Nullable DisplayContent targetDisplay) {
+        final WindowState window;
+        if (targetDisplay == null) {
+            final Task task = new TaskBuilder(mSupervisor).build();
+            final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess(
+                    mProcess).build();
+            window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
+                    activity).setClientWindow(new TestIWindow()).build();
+        } else {
+            window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay(
+                    targetDisplay).setClientWindow(new TestIWindow()).build();
+        }
 
         // Use a new TestIWindow so we don't collect events for other windows
-        final WindowState window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
-                activity).setOwnerId(ownerId).setClientWindow(new TestIWindow()).build();
         InputChannel channel = new InputChannel();
         window.openInputChannel(channel);
         window.mHasSurface = true;
@@ -174,7 +190,7 @@
     public void setUp() throws Exception {
         mTarget = new TestDragDropController(mWm, mWm.mH.getLooper());
         mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc", TEST_PID, TEST_UID);
-        mWindow = createDropTargetWindow("Drag test window", 0);
+        mWindow = createDropTargetWindow("Drag test window");
         doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0);
         when(mWm.mInputManager.startDragAndDrop(any(IBinder.class), any(IBinder.class))).thenReturn(
                 true);
@@ -263,8 +279,8 @@
 
     @Test
     public void testPrivateInterceptGlobalDragDropIgnoresNonLocalWindows() {
-        WindowState nonLocalWindow = createDropTargetWindow("App drag test window", 0);
-        WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window", 0);
+        WindowState nonLocalWindow = createDropTargetWindow("App drag test window");
+        WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window");
         globalInterceptWindow.mAttrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
 
         // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -347,6 +363,120 @@
                 });
     }
 
+    @Test
+    public void testDragEventCoordinates() {
+        int dragStartX = mWindow.getBounds().centerX();
+        int dragStartY = mWindow.getBounds().centerY();
+        int startOffsetPx = 10;
+        int dropCoordsPx = 15;
+        WindowState window2 = createDropTargetWindow("App drag test window");
+        Rect bounds = new Rect(dragStartX + startOffsetPx, dragStartY + startOffsetPx,
+                mWindow.getBounds().right, mWindow.getBounds().bottom);
+        window2.setBounds(bounds);
+        window2.getFrame().set(bounds);
+
+        // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
+        // immediately after dispatching, which is a problem when using mockito arguments captor
+        // because it returns and modifies the same drag event.
+        TestIWindow iwindow = (TestIWindow) mWindow.mClient;
+        final ArrayList<DragEvent> dragEvents = new ArrayList<>();
+        iwindow.setDragEventJournal(dragEvents);
+        TestIWindow iwindow2 = (TestIWindow) window2.mClient;
+        final ArrayList<DragEvent> dragEvents2 = new ArrayList<>();
+        iwindow2.setDragEventJournal(dragEvents2);
+
+        startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+                ClipData.newPlainText("label", "text"), () -> {
+                    // Verify the start-drag event is sent as-is for the drag origin window.
+                    final DragEvent dragEvent = dragEvents.get(0);
+                    assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction());
+                    assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */);
+                    assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */);
+                    // Verify the start-drag event is sent relative to the window top-left.
+                    final DragEvent dragEvent2 = dragEvents2.get(0);
+                    assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction());
+                    assertEquals(-startOffsetPx, dragEvent2.getX(),  0.0 /* delta */);
+                    assertEquals(-startOffsetPx, dragEvent2.getY(), 0.0 /* delta */);
+
+                    try {
+                        mTarget.mDeferDragStateClosed = true;
+                        // x, y is window-local coordinate.
+                        mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx,
+                                dropCoordsPx);
+                        mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx,
+                                dropCoordsPx);
+                        mToken = window2.mClient.asBinder();
+                        // Verify only window2 received the DROP event and coords are sent as-is.
+                        assertEquals(1, dragEvents.size());
+                        assertEquals(2, dragEvents2.size());
+                        final DragEvent dropEvent = last(dragEvents2);
+                        assertEquals(ACTION_DROP, dropEvent.getAction());
+                        assertEquals(dropCoordsPx, dropEvent.getX(),  0.0 /* delta */);
+                        assertEquals(dropCoordsPx, dropEvent.getY(),  0.0 /* delta */);
+
+                        mTarget.reportDropResult(iwindow2, true);
+                    } finally {
+                        mTarget.mDeferDragStateClosed = false;
+                    }
+                });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND)
+    public void testDragEventConnectedDisplaysCoordinates() {
+        final DisplayContent testDisplay = createMockSimulatedDisplay();
+        int dragStartX = mWindow.getBounds().centerX();
+        int dragStartY = mWindow.getBounds().centerY();
+        int dropCoordsPx = 15;
+        WindowState window2 = createDropTargetWindow("App drag test window", testDisplay);
+
+        // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
+        // immediately after dispatching, which is a problem when using mockito arguments captor
+        // because it returns and modifies the same drag event.
+        TestIWindow iwindow = (TestIWindow) mWindow.mClient;
+        final ArrayList<DragEvent> dragEvents = new ArrayList<>();
+        iwindow.setDragEventJournal(dragEvents);
+        TestIWindow iwindow2 = (TestIWindow) window2.mClient;
+        final ArrayList<DragEvent> dragEvents2 = new ArrayList<>();
+        iwindow2.setDragEventJournal(dragEvents2);
+
+        startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+                ClipData.newPlainText("label", "text"), () -> {
+                    // Verify the start-drag event is sent as-is for the drag origin window.
+                    final DragEvent dragEvent = dragEvents.get(0);
+                    assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction());
+                    assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */);
+                    assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */);
+                    // Verify the start-drag event from different display is sent out of display
+                    // bounds.
+                    final DragEvent dragEvent2 = dragEvents2.get(0);
+                    assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction());
+                    assertEquals(-window2.getBounds().left - 1, dragEvent2.getX(), 0.0 /* delta */);
+                    assertEquals(-window2.getBounds().top - 1, dragEvent2.getY(), 0.0 /* delta */);
+
+                    try {
+                        mTarget.mDeferDragStateClosed = true;
+                        // x, y is window-local coordinate.
+                        mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx,
+                                dropCoordsPx);
+                        mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx,
+                                dropCoordsPx);
+                        mToken = window2.mClient.asBinder();
+                        // Verify only window2 received the DROP event and coords are sent as-is
+                        assertEquals(1, dragEvents.size());
+                        assertEquals(2, dragEvents2.size());
+                        final DragEvent dropEvent = last(dragEvents2);
+                        assertEquals(ACTION_DROP, dropEvent.getAction());
+                        assertEquals(dropCoordsPx, dropEvent.getX(),  0.0 /* delta */);
+                        assertEquals(dropCoordsPx, dropEvent.getY(),  0.0 /* delta */);
+
+                        mTarget.reportDropResult(iwindow2, true);
+                    } finally {
+                        mTarget.mDeferDragStateClosed = false;
+                    }
+                });
+    }
+
     private DragEvent last(ArrayList<DragEvent> list) {
         return list.get(list.size() - 1);
     }
@@ -503,7 +633,7 @@
 
     @Test
     public void testRequestSurfaceForReturnAnimationFlag_dropSuccessful() {
-        WindowState otherWindow = createDropTargetWindow("App drag test window", 0);
+        WindowState otherWindow = createDropTargetWindow("App drag test window");
         TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient;
 
         // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -534,7 +664,7 @@
 
     @Test
     public void testRequestSurfaceForReturnAnimationFlag_dropUnsuccessful() {
-        WindowState otherWindow = createDropTargetWindow("App drag test window", 0);
+        WindowState otherWindow = createDropTargetWindow("App drag test window");
         TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient;
 
         // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -687,6 +817,14 @@
      * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
      */
     private void startDrag(int flag, ClipData data, Runnable r) {
+        startDrag(0, 0, flag, data, r);
+    }
+
+    /**
+     * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
+     */
+    private void startDrag(float startInWindowX, float startInWindowY, int flag, ClipData data,
+            Runnable r) {
         final SurfaceSession appSession = new SurfaceSession();
         try {
             final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName(
@@ -694,8 +832,8 @@
                     PixelFormat.TRANSLUCENT).build();
 
             assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder()));
-            mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0,
-                    0, 0, data);
+            mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0,
+                    startInWindowX, startInWindowY, 0, 0, data);
             assertNotNull(mToken);
 
             r.run();
diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
index 3e87f1f..ee9673f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
@@ -177,15 +177,16 @@
         assertTrue("Target didn't call callback enough times.",
                 mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
 
+        // Wait until writing thread is waiting, which indicates the thread is waiting for new tasks
+        // to appear.
+        assertTrue("Failed to wait until the writing thread is waiting.",
+                mTarget.waitUntilWritingThreadIsWaiting(TIMEOUT_ALLOWANCE));
+
         // Second item
         mFactory.setExpectedProcessedItemNumber(1);
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         dispatchTime = SystemClock.uptimeMillis();
-        // Synchronize on the instance to make sure we schedule the item after it starts to wait for
-        // task indefinitely.
-        synchronized (mTarget) {
-            mTarget.addItem(mFactory.createItem(), false);
-        }
+        mTarget.addItem(mFactory.createItem(), false);
         assertTrue("Target didn't process item enough times.",
                 mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
         assertEquals("Target didn't process all items.", 2, mFactory.getTotalProcessedItemCount());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index d1f5d15..be79160 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -76,6 +76,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.app.IApplicationThread;
 import android.content.pm.ActivityInfo;
@@ -1522,7 +1523,7 @@
     @EnableFlags(Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE)
     public void setConfigurationChangeSettingsForUser_createsFromParcel_callsSettingImpl()
             throws Settings.SettingNotFoundException {
-        final int userId = 0;
+        final int currentUserId = ActivityManager.getCurrentUser();
         final int forcedDensity = 400;
         final float forcedFontScaleFactor = 1.15f;
         final Parcelable.Creator<ConfigurationChangeSetting> creator =
@@ -1536,10 +1537,10 @@
 
         mWm.setConfigurationChangeSettingsForUser(settings, UserHandle.USER_CURRENT);
 
-        verify(mDisplayContent).setForcedDensity(forcedDensity, userId);
+        verify(mDisplayContent).setForcedDensity(forcedDensity, currentUserId);
         assertEquals(forcedFontScaleFactor, Settings.System.getFloat(
                 mContext.getContentResolver(), Settings.System.FONT_SCALE), 0.1f /* delta */);
-        verify(mAtm).updateFontScaleIfNeeded(userId);
+        verify(mAtm).updateFontScaleIfNeeded(currentUserId);
     }
 
     @Test
diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
index 060133d..e7e3d10 100644
--- a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
+++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
@@ -81,7 +81,8 @@
         thrown.expect(SecurityException.class);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.reportChange(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -90,7 +91,8 @@
         mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.reportChange(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -99,7 +101,7 @@
         thrown.expect(SecurityException.class);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.reportChangeByPackageName(1, packageName, 0);
+        mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
@@ -108,7 +110,7 @@
         mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.reportChangeByPackageName(1, packageName, 0);
+        mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
@@ -133,7 +135,8 @@
         thrown.expect(SecurityException.class);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.isChangeEnabled(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -143,7 +146,8 @@
         mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.isChangeEnabled(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -152,7 +156,8 @@
         mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.isChangeEnabled(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -161,7 +166,7 @@
         thrown.expect(SecurityException.class);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
@@ -171,7 +176,7 @@
         mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
@@ -180,7 +185,7 @@
         mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
     }
 
     @Test