Merge "Replace IMMS#mSettings with IMMS#mCurrentUserId (2nd try)" into main
diff --git a/apct-tests/perftests/packagemanager/AndroidTest.xml b/apct-tests/perftests/packagemanager/AndroidTest.xml
index c9d45a6..db938e4 100644
--- a/apct-tests/perftests/packagemanager/AndroidTest.xml
+++ b/apct-tests/perftests/packagemanager/AndroidTest.xml
@@ -76,11 +76,6 @@
         <option name="test-file-name" value="QueriesAll49.apk"/>
     </target_preparer>
 
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
-        <option name="package" value="com.android.perftests.packagemanager"/>
-        <option name="hidden-api-checks" value="false"/>
-    </test>
-
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="directory-keys" value="/data/local/PackageManagerPerfTests"/>
         <option name="collect-on-run-ended-only" value="true"/>
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 6246dd7..91cdf8d 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -124,6 +124,22 @@
             "vcn_network_selection_ipsec_packet_loss_percent_threshold";
 
     /**
+     * Key for detecting unusually large increases in IPsec packet sequence numbers.
+     *
+     * <p>If the sequence number increases by more than this value within a second, it may indicate
+     * an intentional leap on the server's downlink. To avoid false positives, the packet loss
+     * detector will suppress loss reporting.
+     *
+     * <p>By default, there's no maximum limit enforced, prioritizing detection of lossy networks.
+     * To reduce false positives, consider setting an appropriate maximum threshold.
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY =
+            "vcn_network_selection_max_seq_num_increase_per_second";
+
+    /**
      * Key for the list of timeouts in minute to stop penalizing an underlying network candidate
      *
      * @hide
@@ -180,6 +196,7 @@
                 VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
                 VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
                 VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
+                VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY,
                 VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY,
                 VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
                 VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY,
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index e64823a..6fde398 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -34,4 +34,14 @@
     namespace: "vcn"
     description: "Re-evaluate IPsec packet loss on LinkProperties or NetworkCapabilities change"
     bug: "323238888"
+}
+
+flag{
+    name: "handle_seq_num_leap"
+    namespace: "vcn"
+    description: "Do not report bad network when there is a suspected sequence number leap"
+    bug: "332598276"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
 }
\ No newline at end of file
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index c95d6ff..a23df79 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -951,7 +951,7 @@
 
     private boolean performSurfaceTransaction(ViewRootImpl viewRoot, Translator translator,
             boolean creating, boolean sizeChanged, boolean hintChanged, boolean relativeZChanged,
-            Transaction surfaceUpdateTransaction) {
+            boolean hdrHeadroomChanged, Transaction surfaceUpdateTransaction) {
         boolean realSizeChanged = false;
 
         mSurfaceLock.lock();
@@ -986,7 +986,7 @@
 
             updateBackgroundVisibility(surfaceUpdateTransaction);
             updateBackgroundColor(surfaceUpdateTransaction);
-            if (mLimitedHdrEnabled) {
+            if (mLimitedHdrEnabled && hdrHeadroomChanged) {
                 surfaceUpdateTransaction.setDesiredHdrHeadroom(
                         mBlastSurfaceControl, mHdrHeadroom);
             }
@@ -1203,7 +1203,7 @@
                 }
 
                 final boolean realSizeChanged = performSurfaceTransaction(viewRoot, translator,
-                        creating, sizeChanged, hintChanged, relativeZChanged,
+                        creating, sizeChanged, hintChanged, relativeZChanged, hdrHeadroomChanged,
                         surfaceUpdateTransaction);
 
                 try {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index cacf0d2..ac1f646 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -2428,6 +2428,26 @@
      */
     public static final int FRAME_RATE_CATEGORY_REASON_IDLE = 0x0700_0000;
 
+    /**
+     * This indicates that the frame rate category was chosen because it is currently boosting.
+     * @hide
+     */
+    public static final int FRAME_RATE_CATEGORY_REASON_BOOST = 0x0800_0000;
+
+    /**
+     * This indicates that the frame rate category was chosen because it is currently having
+     * touch boost.
+     * @hide
+     */
+    public static final int FRAME_RATE_CATEGORY_REASON_TOUCH = 0x0900_0000;
+
+    /**
+     * This indicates that the frame rate category was chosen because it is currently having
+     * touch boost.
+     * @hide
+     */
+    public static final int FRAME_RATE_CATEGORY_REASON_CONFLICTED = 0x0A00_0000;
+
     private static final int FRAME_RATE_CATEGORY_REASON_MASK = 0xFFFF_0000;
 
     /**
@@ -5742,7 +5762,7 @@
      */
     private static final float FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD = 0.07f;
 
-    private static final float MAX_FRAME_RATE = 140;
+    static final float MAX_FRAME_RATE = 140;
 
     private static final int INFREQUENT_UPDATE_INTERVAL_MILLIS = 100;
     private static final int INFREQUENT_UPDATE_COUNTS = 2;
@@ -33897,36 +33917,41 @@
         int height = mBottom - mTop;
 
         if (viewRootImpl != null && (width != 0 && height != 0)) {
-            if (mAttachInfo.mViewVelocityApi) {
-                float velocity = mFrameContentVelocity;
-                int mask = PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN;
-                float frameRate = 0;
+            if (viewRootImpl.shouldCheckFrameRate(mPreferredFrameRate > 0f)) {
+                float velocityFrameRate = 0f;
+                if (mAttachInfo.mViewVelocityApi) {
+                    float velocity = mFrameContentVelocity;
 
-                if (velocity < 0f
-                        && (mPrivateFlags4 & mask) == mask
-                        && mParent instanceof View
-                        && ((View) mParent).mFrameContentVelocity <= 0
-                ) {
-                    // This current calculation is very simple. If something on the screen moved,
-                    // then it votes for the highest velocity. If it doesn't move, then return 0.
-                    velocity = Float.POSITIVE_INFINITY;
-                    frameRate = MAX_FRAME_RATE;
-                }
-                if (velocity > 0f) {
-                    if (sToolkitFrameRateVelocityMappingReadOnlyFlagValue) {
-                        frameRate = convertVelocityToFrameRate(velocity);
+                    if (velocity < 0f
+                            && (mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == (
+                            PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)
+                            && mParent instanceof View
+                            && ((View) mParent).mFrameContentVelocity <= 0
+                    ) {
+                        // This current calculation is very simple. If something on the screen
+                        // moved, then it votes for the highest velocity.
+                        velocityFrameRate = MAX_FRAME_RATE;
+                    } else if (velocity > 0f) {
+                        velocityFrameRate = convertVelocityToFrameRate(velocity);
                     }
-                    viewRootImpl.votePreferredFrameRate(frameRate, FRAME_RATE_COMPATIBILITY_GTE);
-                    return;
+                }
+                if (velocityFrameRate > 0f || mPreferredFrameRate > 0f) {
+                    int compatibility = FRAME_RATE_COMPATIBILITY_GTE;
+                    float frameRate = velocityFrameRate;
+                    if (mPreferredFrameRate > velocityFrameRate) {
+                        compatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
+                        frameRate = mPreferredFrameRate;
+                    }
+                    viewRootImpl.votePreferredFrameRate(frameRate, compatibility);
                 }
             }
-            if (!willNotDraw() && isDirty()) {
+            if (!willNotDraw() && isDirty() && viewRootImpl.shouldCheckFrameRateCategory()) {
                 if (sToolkitMetricsForFrameRateDecisionFlagValue) {
                     float sizePercentage = width * height / mAttachInfo.mDisplayPixelCount;
                     viewRootImpl.recordViewPercentage(sizePercentage);
                 }
 
-                int frameRateCategory;
+                int frameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
                 if (Float.isNaN(mPreferredFrameRate)) {
                     frameRateCategory = calculateFrameRateCategory();
                 } else if (mPreferredFrameRate < 0) {
@@ -33951,10 +33976,6 @@
                                     | FRAME_RATE_CATEGORY_REASON_INVALID;
                         }
                     }
-                } else {
-                    viewRootImpl.votePreferredFrameRate(mPreferredFrameRate,
-                            mFrameRateCompatibility);
-                    return;
                 }
 
                 int category = frameRateCategory & ~FRAME_RATE_CATEGORY_REASON_MASK;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 4024e3c..87dfa029 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -35,14 +35,18 @@
 import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
 import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
 import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE;
+import static android.view.View.FRAME_RATE_CATEGORY_REASON_BOOST;
+import static android.view.View.FRAME_RATE_CATEGORY_REASON_CONFLICTED;
 import static android.view.View.FRAME_RATE_CATEGORY_REASON_IDLE;
 import static android.view.View.FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
 import static android.view.View.FRAME_RATE_CATEGORY_REASON_INVALID;
 import static android.view.View.FRAME_RATE_CATEGORY_REASON_LARGE;
 import static android.view.View.FRAME_RATE_CATEGORY_REASON_REQUESTED;
 import static android.view.View.FRAME_RATE_CATEGORY_REASON_SMALL;
+import static android.view.View.FRAME_RATE_CATEGORY_REASON_TOUCH;
 import static android.view.View.FRAME_RATE_CATEGORY_REASON_UNKNOWN;
 import static android.view.View.FRAME_RATE_CATEGORY_REASON_VELOCITY;
+import static android.view.View.MAX_FRAME_RATE;
 import static android.view.View.PFLAG_DRAW_ANIMATION;
 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
 import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
@@ -4191,8 +4195,15 @@
         // For the variable refresh rate project.
         // We set the preferred frame rate and frame rate category at the end of performTraversals
         // when the values are applicable.
+        setCategoryFromCategoryCounts();
         setPreferredFrameRate(mPreferredFrameRate);
         setPreferredFrameRateCategory(mPreferredFrameRateCategory);
+        if (!mIsFrameRateConflicted) {
+            mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
+            mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING,
+                    FRAME_RATE_SETTING_REEVALUATE_TIME);
+        }
+        checkIdleness();
         mFrameRateCategoryHighCount = mFrameRateCategoryHighCount > 0
                 ? mFrameRateCategoryHighCount - 1 : mFrameRateCategoryHighCount;
         mFrameRateCategoryNormalCount = mFrameRateCategoryNormalCount > 0
@@ -4201,7 +4212,6 @@
                 ? mFrameRateCategoryLowCount - 1 : mFrameRateCategoryLowCount;
         mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_DEFAULT;
         mPreferredFrameRate = -1;
-        mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
         mIsFrameRateConflicted = false;
         mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_UNKNOWN;
     }
@@ -6630,8 +6640,6 @@
                      */
                     mIsFrameRateBoosting = false;
                     mIsTouchBoosting = false;
-                    setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory,
-                            mLastPreferredFrameRateCategory));
                     break;
                 case MSG_CHECK_INVALIDATION_IDLE:
                     if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) {
@@ -7677,7 +7685,6 @@
                     mWindowAttributes.type)) {
                 // set the frame rate to the maximum value.
                 mIsTouchBoosting = true;
-                setPreferredFrameRateCategory(mPreferredFrameRateCategory);
             }
             /**
              * We want to lower the refresh rate when MotionEvent.ACTION_UP,
@@ -12469,59 +12476,50 @@
         EventLog.writeEvent(LOGTAG_VIEWROOT_DRAW_EVENT, mTag, msg);
     }
 
+    /**
+     * Sets the mPreferredFrameRateCategory from the high, high_hint, normal, and low counts.
+     */
+    private void setCategoryFromCategoryCounts() {
+        if (mFrameRateCategoryHighCount > 0) {
+            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_HIGH;
+        } else if (mFrameRateCategoryHighHintCount > 0) {
+            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_HIGH_HINT;
+        } else if (mFrameRateCategoryNormalCount > 0) {
+            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NORMAL;
+        } else if (mFrameRateCategoryLowCount > 0) {
+            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_LOW;
+        }
+    }
+
     private void setPreferredFrameRateCategory(int preferredFrameRateCategory) {
-        if (!shouldSetFrameRateCategory()
-                || (mFrameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE
-                && mPreferredFrameRate > 0
-                && sToolkitFrameRateVelocityMappingReadOnlyFlagValue)) {
+        if (!shouldSetFrameRateCategory()) {
             return;
         }
-        int categoryFromConflictedFrameRates = FRAME_RATE_CATEGORY_DEFAULT;
-        if (mIsFrameRateConflicted) {
-            categoryFromConflictedFrameRates = mPreferredFrameRate > 60
-                    ? FRAME_RATE_CATEGORY_HIGH : FRAME_RATE_CATEGORY_NORMAL;
-        }
 
-        int frameRateCategory = mIsTouchBoosting
-                ? FRAME_RATE_CATEGORY_HIGH_HINT
-                : Math.max(preferredFrameRateCategory, categoryFromConflictedFrameRates);
+        int frameRateCategory;
+        int frameRateReason;
+        String view;
 
-        // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT
-        // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction.
-        // FRAME_RATE_CATEGORY_HIGH is for boosting without user interaction
-        // (e.g., Window Initialization).
-        if (mIsFrameRateBoosting || mInsetsAnimationRunning
-                || (mFrameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE
-                        && mPreferredFrameRate > 0)) {
+        if (mIsFrameRateBoosting || mInsetsAnimationRunning) {
             frameRateCategory = FRAME_RATE_CATEGORY_HIGH;
-            if (mFrameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE) {
-                // We've received a velocity, so we'll let the velocity control the
-                // frame rate unless we receive additional motion events.
-                mIsTouchBoosting = false;
-                mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_VELOCITY;
-                mFrameRateCategoryView = null;
-            } else {
-                mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_UNKNOWN;
-            }
+            frameRateReason = FRAME_RATE_CATEGORY_REASON_BOOST;
+            view = null;
+        } else if (mIsTouchBoosting && preferredFrameRateCategory < FRAME_RATE_CATEGORY_HIGH_HINT) {
+            frameRateCategory = FRAME_RATE_CATEGORY_HIGH_HINT;
+            frameRateReason = FRAME_RATE_CATEGORY_REASON_TOUCH;
+            view = null;
+        } else {
+            frameRateCategory = preferredFrameRateCategory;
+            frameRateReason = mFrameRateCategoryChangeReason;
+            view = mFrameRateCategoryView;
         }
 
         try {
             if (frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT
                     && mLastPreferredFrameRateCategory != frameRateCategory) {
                 if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
-                    String reason = reasonToString(mFrameRateCategoryChangeReason);
-                    String sourceView = mFrameRateCategoryView == null ? "-"
-                            : mFrameRateCategoryView;
-                    if (preferredFrameRateCategory == FRAME_RATE_CATEGORY_HIGH_HINT) {
-                        reason = "touch boost";
-                        sourceView = "-";
-                    } else if (categoryFromConflictedFrameRates == frameRateCategory
-                            && frameRateCategory != preferredFrameRateCategory
-                            && mIsFrameRateConflicted
-                    ) {
-                        reason = "conflict";
-                        sourceView = "-";
-                    }
+                    String reason = reasonToString(frameRateReason);
+                    String sourceView = view == null ? "-" : view;
                     String category = categoryToString(frameRateCategory);
                     Trace.traceBegin(
                             Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRateCategory "
@@ -12565,24 +12563,21 @@
             case FRAME_RATE_CATEGORY_REASON_VELOCITY -> str = "velocity";
             case FRAME_RATE_CATEGORY_REASON_IDLE -> str = "idle";
             case FRAME_RATE_CATEGORY_REASON_UNKNOWN -> str = "unknown";
+            case FRAME_RATE_CATEGORY_REASON_BOOST -> str = "boost";
+            case FRAME_RATE_CATEGORY_REASON_TOUCH -> str = "touch";
+            case FRAME_RATE_CATEGORY_REASON_CONFLICTED -> str = "conflicted";
             default -> str = String.valueOf(reason);
         }
         return str;
     }
 
     private void setPreferredFrameRate(float preferredFrameRate) {
-        if (!shouldSetFrameRate()) {
-            return;
-        }
-        if (mFrameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE
-                && preferredFrameRate > 0 && !sToolkitFrameRateVelocityMappingReadOnlyFlagValue) {
-            mIsTouchBoosting = false;
+        if (!shouldSetFrameRate() || preferredFrameRate < 0) {
             return;
         }
 
         try {
-            if (mLastPreferredFrameRate != preferredFrameRate
-                    && preferredFrameRate >= 0) {
+            if (mLastPreferredFrameRate != preferredFrameRate) {
                 if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                     Trace.traceBegin(
                             Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRate "
@@ -12591,7 +12586,7 @@
                 }
                 if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) {
                     mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate,
-                    mFrameRateCompatibility).applyAsyncUnsafe();
+                            mFrameRateCompatibility).applyAsyncUnsafe();
                 }
                 mLastPreferredFrameRate = preferredFrameRate;
             }
@@ -12602,12 +12597,6 @@
         }
     }
 
-    private void sendDelayedEmptyMessage(int message, int delayedTime) {
-        mHandler.removeMessages(message);
-
-        mHandler.sendEmptyMessageDelayed(message, delayedTime);
-    }
-
     private boolean shouldSetFrameRateCategory() {
         // use toolkitSetFrameRate flag to gate the change
         return  mSurface.isValid() && shouldEnableDvrr();
@@ -12645,28 +12634,34 @@
             case FRAME_RATE_CATEGORY_HIGH ->
                     mFrameRateCategoryHighCount = FRAME_RATE_CATEGORY_COUNT;
         }
-
-        int oldCategory = mPreferredFrameRateCategory;
-        // For View that votes NO_PREFERENCE
-        mPreferredFrameRateCategory = frameRateCategory;
-
-        if (mFrameRateCategoryHighCount > 0) {
-            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_HIGH;
-        } else if (mFrameRateCategoryHighHintCount > 0) {
-            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_HIGH_HINT;
-        } else if (mFrameRateCategoryNormalCount > 0) {
-            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NORMAL;
-        } else if (mFrameRateCategoryLowCount > 0) {
-            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_LOW;
+        if (frameRateCategory > mPreferredFrameRateCategory) {
+            mPreferredFrameRateCategory = frameRateCategory;
+            mFrameRateCategoryChangeReason = reason;
+            mFrameRateCategoryView = view == null ? "-" : view.getClass().getSimpleName();
         }
         mHasInvalidation = true;
-        checkIdleness();
-        if (mPreferredFrameRateCategory != oldCategory
-                && mPreferredFrameRateCategory == frameRateCategory
-        ) {
-            mFrameRateCategoryChangeReason = reason;
-            mFrameRateCategoryView = view == null ? "null" : view.getClass().getSimpleName();
-        }
+    }
+
+    /**
+     * Returns whether a View should vote for frame rate category. When the category is HIGH
+     * already, there's no need to calculate the category on the View and vote.
+     */
+    public boolean shouldCheckFrameRateCategory() {
+        return mPreferredFrameRateCategory < FRAME_RATE_CATEGORY_HIGH;
+    }
+
+    /**
+     * Returns whether a View should vote for frame rate. When the maximum frame rate has already
+     * been voted for, there's no point in calculating and voting for the frame rate. When
+     * isDirect is false, then it will return false when the velocity-calculated frame rate
+     * can be avoided.
+     * @param isDirect true when the frame rate has been set directly on the View or false if
+     *                 the calculation is based only on velocity.
+     */
+    public boolean shouldCheckFrameRate(boolean isDirect) {
+        return mPreferredFrameRate < MAX_FRAME_RATE
+                || (!isDirect && !sToolkitFrameRateVelocityMappingReadOnlyFlagValue
+                && mPreferredFrameRateCategory < FRAME_RATE_CATEGORY_HIGH);
     }
 
     /**
@@ -12692,24 +12687,44 @@
         if (frameRate <= 0) {
             return;
         }
+        if (frameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE) {
+            mIsTouchBoosting = false;
+            if (!sToolkitFrameRateVelocityMappingReadOnlyFlagValue) {
+                mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_HIGH;
+                mFrameRateCategoryHighCount = FRAME_RATE_CATEGORY_COUNT;
+                mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_VELOCITY;
+                mFrameRateCategoryView = null;
+                return;
+            }
+        }
+        float nextFrameRate;
+        int nextFrameRateCompatibility;
+        if (frameRate > mPreferredFrameRate) {
+            nextFrameRate = frameRate;
+            nextFrameRateCompatibility = frameRateCompatibility;
+        } else {
+            nextFrameRate = mPreferredFrameRate;
+            nextFrameRateCompatibility = mFrameRateCompatibility;
+        }
+
         if (mPreferredFrameRate > 0 && mPreferredFrameRate % frameRate != 0
                 && frameRate % mPreferredFrameRate != 0) {
             mIsFrameRateConflicted = true;
-            mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
-        }
-        if (frameRate > mPreferredFrameRate) {
-            mFrameRateCompatibility = frameRateCompatibility;
+            if (nextFrameRate > 60 && mFrameRateCategoryHighCount != FRAME_RATE_CATEGORY_COUNT) {
+                mFrameRateCategoryHighCount = FRAME_RATE_CATEGORY_COUNT;
+                mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_CONFLICTED;
+                mFrameRateCategoryView = null;
+            } else if (mFrameRateCategoryHighCount == 0 && mFrameRateCategoryHighHintCount == 0
+                    && mFrameRateCategoryNormalCount < FRAME_RATE_CATEGORY_COUNT) {
+                mFrameRateCategoryNormalCount = FRAME_RATE_CATEGORY_COUNT;
+                mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_CONFLICTED;
+                mFrameRateCategoryView = null;
+            }
         }
 
-        mPreferredFrameRate = Math.max(mPreferredFrameRate, frameRate);
+        mPreferredFrameRate = nextFrameRate;
+        mFrameRateCompatibility = nextFrameRateCompatibility;
         mHasInvalidation = true;
-
-        if (!mIsFrameRateConflicted) {
-            mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
-            mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING,
-                    FRAME_RATE_SETTING_REEVALUATE_TIME);
-        }
-        checkIdleness();
     }
 
     /**
@@ -12779,7 +12794,6 @@
 
     private void boostFrameRate(int boostTimeOut) {
         mIsFrameRateBoosting = true;
-        setPreferredFrameRateCategory(mPreferredFrameRateCategory);
         mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT);
         mHandler.sendEmptyMessageDelayed(MSG_TOUCH_BOOST_TIMEOUT,
                 boostTimeOut);
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.cpp b/core/jni/com_android_internal_content_FileSystemUtils.cpp
index 4bd2d72..01920de 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.cpp
+++ b/core/jni/com_android_internal_content_FileSystemUtils.cpp
@@ -42,7 +42,11 @@
 bool punchHoles(const char *filePath, const uint64_t offset,
                 const std::vector<Elf64_Phdr> &programHeaders) {
     struct stat64 beforePunch;
-    lstat64(filePath, &beforePunch);
+    if (int result = lstat64(filePath, &beforePunch); result != 0) {
+        ALOGE("lstat64 failed for filePath %s, error:%d", filePath, errno);
+        return false;
+    }
+
     uint64_t blockSize = beforePunch.st_blksize;
     IF_ALOGD() {
         ALOGD("Total number of LOAD segments %zu", programHeaders.size());
@@ -152,7 +156,10 @@
 
     IF_ALOGD() {
         struct stat64 afterPunch;
-        lstat64(filePath, &afterPunch);
+        if (int result = lstat64(filePath, &afterPunch); result != 0) {
+            ALOGD("lstat64 failed for filePath %s, error:%d", filePath, errno);
+            return false;
+        }
         ALOGD("Size after punching holes st_blocks: %" PRIu64 ", st_blksize: %ld, st_size: %" PRIu64
               "",
               afterPunch.st_blocks, afterPunch.st_blksize,
@@ -177,7 +184,7 @@
 
     // only consider elf64 for punching holes
     if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) {
-        ALOGE("Provided file is not ELF64");
+        ALOGW("Provided file is not ELF64");
         return false;
     }
 
@@ -215,4 +222,108 @@
     return punchHoles(filePath, offset, programHeaders);
 }
 
+bool punchHolesInZip(const char *filePath, uint64_t offset, uint16_t extraFieldLen) {
+    android::base::unique_fd fd(open(filePath, O_RDWR | O_CLOEXEC));
+    if (!fd.ok()) {
+        ALOGE("Can't open file to punch %s", filePath);
+        return false;
+    }
+
+    struct stat64 beforePunch;
+    if (int result = lstat64(filePath, &beforePunch); result != 0) {
+        ALOGE("lstat64 failed for filePath %s, error:%d", filePath, errno);
+        return false;
+    }
+
+    uint64_t blockSize = beforePunch.st_blksize;
+    IF_ALOGD() {
+        ALOGD("Extra field length: %hu,  Size before punching holes st_blocks: %" PRIu64
+              ", st_blksize: %ld, st_size: %" PRIu64 "",
+              extraFieldLen, beforePunch.st_blocks, beforePunch.st_blksize,
+              static_cast<uint64_t>(beforePunch.st_size));
+    }
+
+    if (extraFieldLen < blockSize) {
+        ALOGD("Skipping punching apk as extra field length is less than block size");
+        return false;
+    }
+
+    // content is preceded by extra field. Zip offset is offset of exact content.
+    // move back by extraFieldLen so that scan can be started at start of extra field.
+    uint64_t extraFieldStart;
+    if (__builtin_sub_overflow(offset, extraFieldLen, &extraFieldStart)) {
+        ALOGE("Overflow occurred when calculating start of extra field");
+        return false;
+    }
+
+    constexpr uint64_t kMaxSize = 64 * 1024;
+    // Use malloc to gracefully handle any oom conditions
+    std::unique_ptr<uint8_t, decltype(&free)> buffer(static_cast<uint8_t *>(malloc(kMaxSize)),
+                                                     &free);
+    if (buffer == nullptr) {
+        ALOGE("Failed to allocate read buffer");
+        return false;
+    }
+
+    // Read the entire extra fields at once and punch file according to zero stretches.
+    if (!ReadFullyAtOffset(fd, buffer.get(), extraFieldLen, extraFieldStart)) {
+        ALOGE("Failed to read extra field content");
+        return false;
+    }
+
+    IF_ALOGD() {
+        ALOGD("Extra field length: %hu content near offset: %s", extraFieldLen,
+              HexString(buffer.get(), extraFieldLen).c_str());
+    }
+
+    uint64_t currentSize = 0;
+    while (currentSize < extraFieldLen) {
+        uint64_t end = currentSize;
+        // find zero ranges
+        while (end < extraFieldLen && *(buffer.get() + end) == 0) {
+            ++end;
+        }
+
+        uint64_t punchLen;
+        if (__builtin_sub_overflow(end, currentSize, &punchLen)) {
+            ALOGW("Overflow occurred when calculating punching length");
+            return false;
+        }
+
+        // Don't punch for every stretch of zero which is found
+        if (punchLen > blockSize) {
+            uint64_t punchOffset;
+            if (__builtin_add_overflow(extraFieldStart, currentSize, &punchOffset)) {
+                ALOGW("Overflow occurred when calculating punch start offset");
+                return false;
+            }
+
+            ALOGD("Punching hole in apk start: %" PRIu64 " len:%" PRIu64 "", punchOffset, punchLen);
+
+            // Punch hole for this entire stretch.
+            int result = fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, punchOffset,
+                                   punchLen);
+            if (result < 0) {
+                ALOGE("fallocate failed to punch hole inside apk, error:%d", errno);
+                return false;
+            }
+        }
+        currentSize = end;
+        ++currentSize;
+    }
+
+    IF_ALOGD() {
+        struct stat64 afterPunch;
+        if (int result = lstat64(filePath, &afterPunch); result != 0) {
+            ALOGD("lstat64 failed for filePath %s, error:%d", filePath, errno);
+            return false;
+        }
+        ALOGD("punchHolesInApk:: Size after punching holes st_blocks: %" PRIu64
+              ", st_blksize: %ld, st_size: %" PRIu64 "",
+              afterPunch.st_blocks, afterPunch.st_blksize,
+              static_cast<uint64_t>(afterPunch.st_size));
+    }
+    return true;
+}
+
 }; // namespace android
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.h b/core/jni/com_android_internal_content_FileSystemUtils.h
index a6b145c..52445e2 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.h
+++ b/core/jni/com_android_internal_content_FileSystemUtils.h
@@ -28,4 +28,11 @@
  */
 bool punchHolesInElf64(const char* filePath, uint64_t offset);
 
+/*
+ * This function punches holes in zero segments of Apk file which are introduced during the
+ * alignment. Alignment tools add padding inside of extra field in local file header. punch holes in
+ * extra field for zero stretches till the actual file content.
+ */
+bool punchHolesInZip(const char* filePath, uint64_t offset, uint16_t extraFieldLen);
+
 } // namespace android
\ No newline at end of file
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index faa83f8..9b8dab7 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -28,6 +28,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/stat.h>
+#include <sys/statfs.h>
 #include <sys/types.h>
 #include <time.h>
 #include <unistd.h>
@@ -145,8 +146,9 @@
 
     uint16_t method;
     off64_t offset;
-
-    if (!zipFile->getEntryInfo(zipEntry, &method, &uncompLen, nullptr, &offset, &when, &crc)) {
+    uint16_t extraFieldLength;
+    if (!zipFile->getEntryInfo(zipEntry, &method, &uncompLen, nullptr, &offset, &when, &crc,
+                               &extraFieldLength)) {
         ALOGE("Couldn't read zip entry info\n");
         return INSTALL_FAILED_INVALID_APK;
     }
@@ -177,6 +179,12 @@
                   "%" PRIu64 "",
                   fileName, zipFile->getZipFileName(), offset);
         }
+
+        // if extra field for this zip file is present with some length, possibility is that it is
+        // padding added for zip alignment. Punch holes there too.
+        if (!punchHolesInZip(zipFile->getZipFileName(), offset, extraFieldLength)) {
+            ALOGW("Failed to punch apk : %s at extra field", zipFile->getZipFileName());
+        }
 #endif // ENABLE_PUNCH_HOLES
 
         return INSTALL_SUCCEEDED;
@@ -279,6 +287,25 @@
         return INSTALL_FAILED_CONTAINER_ERROR;
     }
 
+#ifdef ENABLE_PUNCH_HOLES
+    // punch extracted elf files as well. This will fail where compression is on (like f2fs) but it
+    // will be useful for ext4 based systems
+    struct statfs64 fsInfo;
+    int result = statfs64(localFileName, &fsInfo);
+    if (result < 0) {
+        ALOGW("Failed to stat file :%s", localFileName);
+    }
+
+    if (result == 0 && fsInfo.f_type == EXT4_SUPER_MAGIC) {
+        ALOGD("Punching extracted elf file %s on fs:%" PRIu64 "", fileName,
+              static_cast<uint64_t>(fsInfo.f_type));
+        if (!punchHolesInElf64(localFileName, 0)) {
+            ALOGW("Failed to punch extracted elf file :%s from apk : %s", fileName,
+                  zipFile->getZipFileName());
+        }
+    }
+#endif // ENABLE_PUNCH_HOLES
+
     ALOGV("Successfully moved %s to %s\n", localTmpFileName, localFileName);
 
     return INSTALL_SUCCEEDED;
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 28343f1..0bf9a4c 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -203,7 +203,9 @@
         mActivityRule.runOnUiThread(() -> {
             mMovingView.setFrameContentVelocity(1_000_000_000f);
             mMovingView.invalidate();
-            runAfterDraw(() -> assertEquals(140f, mViewRoot.getLastPreferredFrameRate(), 0f));
+            runAfterDraw(() -> {
+                assertEquals(140f, mViewRoot.getLastPreferredFrameRate(), 0f);
+            });
         });
         waitForAfterDraw();
     }
@@ -411,6 +413,26 @@
         waitForAfterDraw();
     }
 
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VELOCITY_MAPPING_READ_ONLY
+    })
+    public void frameRateAndCategory() throws Throwable {
+        waitForFrameRateCategoryToSettle();
+        mActivityRule.runOnUiThread(() -> {
+            mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            mMovingView.setFrameContentVelocity(1f);
+            mMovingView.invalidate();
+            runAfterDraw(() -> {
+                assertEquals(FRAME_RATE_CATEGORY_LOW,
+                        mViewRoot.getLastPreferredFrameRateCategory());
+                assertEquals(60f, mViewRoot.getLastPreferredFrameRate());
+            });
+        });
+        waitForAfterDraw();
+    }
+
     private void runAfterDraw(@NonNull Runnable runnable) {
         Handler handler = new Handler(Looper.getMainLooper());
         mAfterDrawLatch = new CountDownLatch(1);
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index e0282a4..80fef6c 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -52,6 +52,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -806,37 +807,50 @@
             assertEquals(0, mViewRootImpl.getPreferredFrameRate(), 0.1);
             assertEquals(mViewRootImpl.getFrameRateCompatibility(),
                     FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
-            assertEquals(false, mViewRootImpl.isFrameRateConflicted());
+            assertFalse(mViewRootImpl.isFrameRateConflicted());
             mViewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_GTE);
-            assertEquals(24, mViewRootImpl.getPreferredFrameRate(), 0.1);
-            assertEquals(FRAME_RATE_COMPATIBILITY_GTE,
-                    mViewRootImpl.getFrameRateCompatibility());
-            assertEquals(false, mViewRootImpl.isFrameRateConflicted());
+            if (toolkitFrameRateVelocityMappingReadOnly()) {
+                assertEquals(24, mViewRootImpl.getPreferredFrameRate(), 0.1);
+                assertEquals(FRAME_RATE_COMPATIBILITY_GTE,
+                        mViewRootImpl.getFrameRateCompatibility());
+                assertFalse(mViewRootImpl.isFrameRateConflicted());
+            } else {
+                assertEquals(FRAME_RATE_CATEGORY_HIGH,
+                        mViewRootImpl.getPreferredFrameRateCategory());
+            }
             mViewRootImpl.votePreferredFrameRate(30, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
             assertEquals(30, mViewRootImpl.getPreferredFrameRate(), 0.1);
             // If there is a conflict, then set compatibility to
             // FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
             assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                     mViewRootImpl.getFrameRateCompatibility());
-            // Should be true since there is a conflict between 24 and 30.
-            assertTrue(mViewRootImpl.isFrameRateConflicted());
+            if (toolkitFrameRateVelocityMappingReadOnly()) {
+                // Should be true since there is a conflict between 24 and 30.
+                assertTrue(mViewRootImpl.isFrameRateConflicted());
+            }
+
             mView.invalidate();
         });
         sInstrumentation.waitForIdleSync();
 
         sInstrumentation.runOnMainSync(() -> {
-            assertEquals(false, mViewRootImpl.isFrameRateConflicted());
+            assertFalse(mViewRootImpl.isFrameRateConflicted());
             mViewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_GTE);
-            assertEquals(60, mViewRootImpl.getPreferredFrameRate(), 0.1);
-            assertEquals(FRAME_RATE_COMPATIBILITY_GTE,
-                    mViewRootImpl.getFrameRateCompatibility());
-            assertEquals(mViewRootImpl.isFrameRateConflicted(), false);
+            if (toolkitFrameRateVelocityMappingReadOnly()) {
+                assertEquals(60, mViewRootImpl.getPreferredFrameRate(), 0.1);
+                assertEquals(FRAME_RATE_COMPATIBILITY_GTE,
+                        mViewRootImpl.getFrameRateCompatibility());
+            } else {
+                assertEquals(FRAME_RATE_CATEGORY_HIGH,
+                        mViewRootImpl.getPreferredFrameRateCategory());
+            }
+            assertFalse(mViewRootImpl.isFrameRateConflicted());
             mViewRootImpl.votePreferredFrameRate(120, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
             assertEquals(120, mViewRootImpl.getPreferredFrameRate(), 0.1);
             assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                     mViewRootImpl.getFrameRateCompatibility());
             // Should be false since 60 is a divisor of 120.
-            assertEquals(false, mViewRootImpl.isFrameRateConflicted());
+            assertFalse(mViewRootImpl.isFrameRateConflicted());
             mViewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_GTE);
             assertEquals(120, mViewRootImpl.getPreferredFrameRate(), 0.1);
             // compatibility should be remained the same (FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
@@ -844,8 +858,7 @@
             assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                     mViewRootImpl.getFrameRateCompatibility());
             // Should be false since 60 is a divisor of 120.
-            assertEquals(false, mViewRootImpl.isFrameRateConflicted());
-
+            assertFalse(mViewRootImpl.isFrameRateConflicted());
         });
     }
 
@@ -872,7 +885,7 @@
         // reset the frame rate category counts
         for (int i = 0; i < 5; i++) {
             sInstrumentation.runOnMainSync(() -> {
-                mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+                mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
                 mView.invalidate();
             });
             sInstrumentation.waitForIdleSync();
@@ -881,21 +894,21 @@
         waitForFrameRateCategoryToSettle(mView);
 
         sInstrumentation.runOnMainSync(() -> {
-            mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
             mView.invalidate();
             runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_LOW,
                     mViewRootImpl.getLastPreferredFrameRateCategory()));
         });
         waitForAfterDraw();
         sInstrumentation.runOnMainSync(() -> {
-            mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_NORMAL);
+            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NORMAL);
             mView.invalidate();
             runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NORMAL,
                     mViewRootImpl.getLastPreferredFrameRateCategory()));
         });
         waitForAfterDraw();
         sInstrumentation.runOnMainSync(() -> {
-            mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_HIGH);
+            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_HIGH);
             mView.invalidate();
             runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
                     mViewRootImpl.getLastPreferredFrameRateCategory()));
@@ -932,24 +945,20 @@
             assertEquals(0, mViewRootImpl.getPreferredFrameRate(), 0.1);
             mView.setFrameContentVelocity(100);
             mView.invalidate();
-            if (toolkitFrameRateVelocityMappingReadOnly()) {
-                runAfterDraw(() -> assertTrue(mViewRootImpl.getLastPreferredFrameRate() > 0));
-            } else {
-                runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
-                        mViewRootImpl.getLastPreferredFrameRateCategory()));
-            }
+            runAfterDraw(() -> {
+                if (toolkitFrameRateVelocityMappingReadOnly()) {
+                    assertEquals(FRAME_RATE_CATEGORY_LOW,
+                            mViewRootImpl.getLastPreferredFrameRateCategory());
+                    assertTrue(mViewRootImpl.getLastPreferredFrameRate() >= 60f);
+                } else {
+                    assertEquals(FRAME_RATE_CATEGORY_HIGH,
+                            mViewRootImpl.getLastPreferredFrameRateCategory());
+                    assertEquals(0, mViewRootImpl.getLastPreferredFrameRate(), 0.1);
+                }
+            });
         });
         waitForAfterDraw();
         sInstrumentation.waitForIdleSync();
-        if (toolkitFrameRateVelocityMappingReadOnly()) {
-            assertEquals(FRAME_RATE_CATEGORY_HIGH,
-                    mViewRootImpl.getLastPreferredFrameRateCategory());
-            assertTrue(mViewRootImpl.getLastPreferredFrameRate() >= 60f);
-        } else {
-            assertEquals(FRAME_RATE_CATEGORY_HIGH,
-                    mViewRootImpl.getLastPreferredFrameRateCategory());
-            assertEquals(0, mViewRootImpl.getLastPreferredFrameRate(), 0.1);
-        }
     }
 
     /**
@@ -1002,9 +1011,9 @@
 
         ViewRootImpl viewRootImpl = mView.getViewRootImpl();
         final WindowManager.LayoutParams attrs = viewRootImpl.mWindowAttributes;
-        assertEquals(true, attrs.getFrameRateBoostOnTouchEnabled());
-        assertEquals(viewRootImpl.getFrameRateBoostOnTouchEnabled(),
-                attrs.getFrameRateBoostOnTouchEnabled());
+        assertTrue(attrs.getFrameRateBoostOnTouchEnabled());
+        assertEquals(attrs.getFrameRateBoostOnTouchEnabled(),
+                viewRootImpl.getFrameRateBoostOnTouchEnabled());
 
         sInstrumentation.runOnMainSync(() -> {
             attrs.setFrameRateBoostOnTouchEnabled(false);
@@ -1014,9 +1023,9 @@
 
         sInstrumentation.runOnMainSync(() -> {
             final WindowManager.LayoutParams newAttrs = viewRootImpl.mWindowAttributes;
-            assertEquals(false, newAttrs.getFrameRateBoostOnTouchEnabled());
-            assertEquals(viewRootImpl.getFrameRateBoostOnTouchEnabled(),
-                    newAttrs.getFrameRateBoostOnTouchEnabled());
+            assertFalse(newAttrs.getFrameRateBoostOnTouchEnabled());
+            assertEquals(newAttrs.getFrameRateBoostOnTouchEnabled(),
+                    viewRootImpl.getFrameRateBoostOnTouchEnabled());
         });
     }
 
@@ -1040,24 +1049,24 @@
             assertEquals(0, viewRootImpl.getPreferredFrameRate(), 0.1);
             assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                     viewRootImpl.getFrameRateCompatibility());
-            assertEquals(false, viewRootImpl.isFrameRateConflicted());
+            assertFalse(viewRootImpl.isFrameRateConflicted());
             viewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
             assertEquals(24, viewRootImpl.getPreferredFrameRate(), 0.1);
             assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                     viewRootImpl.getFrameRateCompatibility());
-            assertEquals(false, viewRootImpl.isFrameRateConflicted());
+            assertFalse(viewRootImpl.isFrameRateConflicted());
             mView.invalidate();
             assertEquals(24, viewRootImpl.getPreferredFrameRate(), 0.1);
             assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                     viewRootImpl.getFrameRateCompatibility());
-            assertEquals(false, viewRootImpl.isFrameRateConflicted());
+            assertFalse(viewRootImpl.isFrameRateConflicted());
         });
 
         Thread.sleep(delay);
         assertEquals(0, viewRootImpl.getPreferredFrameRate(), 0.1);
         assertEquals(FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
                 viewRootImpl.getFrameRateCompatibility());
-        assertEquals(false, viewRootImpl.isFrameRateConflicted());
+        assertFalse(viewRootImpl.isFrameRateConflicted());
     }
 
     /**
@@ -1093,14 +1102,14 @@
         // reset the frame rate category counts
         for (int i = 0; i < 5; i++) {
             sInstrumentation.runOnMainSync(() -> {
-                mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+                mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
                 mView.invalidate();
             });
             sInstrumentation.waitForIdleSync();
         }
 
         sInstrumentation.runOnMainSync(() -> {
-            mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
             mView.invalidate();
             runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_LOW,
                     mViewRootImpl.getLastPreferredFrameRateCategory()));
@@ -1153,7 +1162,7 @@
         // reset the frame rate category counts
         for (int i = 0; i < 5; i++) {
             sInstrumentation.runOnMainSync(() -> {
-                mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+                mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
                 mView.invalidate();
             });
             sInstrumentation.waitForIdleSync();
@@ -1162,15 +1171,17 @@
         // In transition from frequent update to infrequent update
         Thread.sleep(delay);
         sInstrumentation.runOnMainSync(() -> {
-            mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
             mView.invalidate();
-            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
-                    mViewRootImpl.getLastPreferredFrameRateCategory()));
+            runAfterDraw(() -> {
+                assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                        mViewRootImpl.getLastPreferredFrameRateCategory());
+            });
         });
         waitForAfterDraw();
         Thread.sleep(delay);
         sInstrumentation.runOnMainSync(() -> {
-            mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT);
+            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT);
             mView.invalidate();
             runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
                     mViewRootImpl.getLastPreferredFrameRateCategory()));
@@ -1180,7 +1191,7 @@
         // Infrequent update
         Thread.sleep(delay);
         sInstrumentation.runOnMainSync(() -> {
-            mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT);
+            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT);
             mView.invalidate();
             runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NORMAL,
                     mViewRootImpl.getLastPreferredFrameRateCategory()));
@@ -1201,9 +1212,9 @@
 
         ViewRootImpl viewRoot = mView.getViewRootImpl();
         final WindowManager.LayoutParams attrs = viewRoot.mWindowAttributes;
-        assertEquals(attrs.isFrameRatePowerSavingsBalanced(), true);
-        assertEquals(viewRoot.isFrameRatePowerSavingsBalanced(),
-                attrs.isFrameRatePowerSavingsBalanced());
+        assertTrue(attrs.isFrameRatePowerSavingsBalanced());
+        assertEquals(attrs.isFrameRatePowerSavingsBalanced(),
+                viewRoot.isFrameRatePowerSavingsBalanced());
 
         sInstrumentation.runOnMainSync(() -> {
             attrs.setFrameRatePowerSavingsBalanced(false);
@@ -1213,9 +1224,9 @@
 
         sInstrumentation.runOnMainSync(() -> {
             final WindowManager.LayoutParams newAttrs = viewRoot.mWindowAttributes;
-            assertEquals(false, newAttrs.isFrameRatePowerSavingsBalanced());
-            assertEquals(viewRoot.isFrameRatePowerSavingsBalanced(),
-                    newAttrs.isFrameRatePowerSavingsBalanced());
+            assertFalse(newAttrs.isFrameRatePowerSavingsBalanced());
+            assertEquals(newAttrs.isFrameRatePowerSavingsBalanced(),
+                    viewRoot.isFrameRatePowerSavingsBalanced());
         });
     }
 
@@ -1266,7 +1277,7 @@
         for (int i = 0; i < 5; i++) {
             Thread.sleep(delay);
             sInstrumentation.runOnMainSync(() -> {
-                mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+                mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
                 mView.invalidate();
             });
             sInstrumentation.waitForIdleSync();
@@ -1274,7 +1285,7 @@
 
         Thread.sleep(delay);
         sInstrumentation.runOnMainSync(() -> {
-            mView.setRequestedFrameRate(mView.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT);
+            mView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT);
             mView.invalidate();
             runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_NORMAL,
                     mViewRootImpl.getLastPreferredFrameRateCategory()));
@@ -1410,7 +1421,7 @@
             mViewRootImpl.dispatchInputEvent(event);
         });
         sInstrumentation.waitForIdleSync();
-        assertEquals(mKeyReceived, shouldReceiveKey);
+        assertEquals(shouldReceiveKey, mKeyReceived);
     }
 
     private void attachViewToWindow(View view) {
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 ec2e670..487bbfb 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
@@ -885,8 +885,9 @@
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
     ) {
-        val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
-        val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FREEFORM) {
+        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
+        val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
+        val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
             // Display windowing is freeform, set to undefined and inherit it
             WINDOWING_MODE_UNDEFINED
         } else {
@@ -903,8 +904,9 @@
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
     ) {
-        val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
-        val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
+        val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
+        val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) {
             // Display windowing is fullscreen, set to undefined and inherit it
             WINDOWING_MODE_UNDEFINED
         } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 32f271b..87dc391 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -19,14 +19,19 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.PackageManager.FEATURE_PC;
+import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 
 import android.app.ActivityManager.RunningTaskInfo;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Handler;
+import android.provider.Settings;
 import android.util.SparseArray;
 import android.view.Choreographer;
+import android.view.Display;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -163,10 +168,33 @@
     }
 
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
-        return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
-                || (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
-                && taskInfo.configuration.windowConfiguration.getDisplayWindowingMode()
-                == WINDOWING_MODE_FREEFORM);
+        if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+            return true;
+        }
+        if (taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) {
+            return false;
+        }
+        final DisplayAreaInfo rootDisplayAreaInfo =
+                mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId);
+        if (rootDisplayAreaInfo != null) {
+            return rootDisplayAreaInfo.configuration.windowConfiguration.getWindowingMode()
+                    == WINDOWING_MODE_FREEFORM;
+        }
+
+        // It is possible that the rootDisplayAreaInfo is null when a task appears soon enough after
+        // a new display shows up, because TDA may appear after task appears in WM shell. Instead of
+        // fixing the synchronization issues, let's use other signals to "guess" the answer. It is
+        // OK in this context because no other captions other than the legacy developer option
+        // freeform and Kingyo/CF PC may use this class. WM shell should have full control over the
+        // condition where captions should show up in all new cases such as desktop mode, for which
+        // we should use different window decor view models. Ultimately Kingyo/CF PC may need to
+        // spin up their own window decor view model when they start to care about multiple
+        // displays.
+        if (isPc()) {
+            return true;
+        }
+        return taskInfo.displayId != Display.DEFAULT_DISPLAY
+                && forcesDesktopModeOnExternalDisplays();
     }
 
     private void createWindowDecoration(
@@ -313,4 +341,17 @@
             return true;
         }
     }
+
+    /**
+     * Returns if this device is a PC.
+     */
+    private boolean isPc() {
+        return mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
+    }
+
+    private boolean forcesDesktopModeOnExternalDisplays() {
+        final ContentResolver resolver = mContext.getContentResolver();
+        return Settings.Global.getInt(resolver,
+                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
+    }
 }
\ No newline at end of file
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 93a967e..4d0f11b 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
@@ -161,6 +161,10 @@
                 (i.arguments.first() as Rect).set(STABLE_BOUNDS)
             }
 
+        val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
+
         controller = createController()
         controller.setSplitScreenController(splitScreenController)
 
@@ -336,9 +340,10 @@
     }
 
     @Test
-    fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() {
+    fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
         val task = setUpFullscreenTask()
-        task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
         controller.moveToDesktop(task)
         val wct = getLatestMoveToDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -346,9 +351,10 @@
     }
 
     @Test
-    fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() {
+    fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() {
         val task = setUpFullscreenTask()
-        task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
         controller.moveToDesktop(task)
         val wct = getLatestMoveToDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -481,9 +487,10 @@
     }
 
     @Test
-    fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() {
+    fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
         val task = setUpFreeformTask()
-        task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
         controller.moveToFullscreen(task.taskId)
         val wct = getLatestExitDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -491,9 +498,10 @@
     }
 
     @Test
-    fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() {
+    fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() {
         val task = setUpFreeformTask()
-        task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
         controller.moveToFullscreen(task.taskId)
         val wct = getLatestExitDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -684,7 +692,7 @@
                 createTransition(freeformTask2, type = TRANSIT_TO_FRONT)
             )
         assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode)
-            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
     }
 
     @Test
@@ -694,7 +702,7 @@
         val task = createFreeformTask()
         val result = controller.handleRequest(Binder(), createTransition(task))
         assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
-            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
     }
 
     @Test
@@ -706,7 +714,7 @@
 
         val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
         assertThat(result?.changes?.get(taskDefaultDisplay.token.asBinder())?.windowingMode)
-            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
     }
 
     @Test
@@ -792,7 +800,7 @@
 
         val result = controller.handleRequest(Binder(), createTransition(task))
         assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
-                .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+                .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
     }
 
     @Test
@@ -895,7 +903,7 @@
 
         val wct = getLatestExitDesktopWct()
         assertThat(wct.changes[task2.token.asBinder()]?.windowingMode)
-                .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+                .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
     }
 
     @Test
diff --git a/libs/androidfw/ZipFileRO.cpp b/libs/androidfw/ZipFileRO.cpp
index 34a6bc2..839c7b6 100644
--- a/libs/androidfw/ZipFileRO.cpp
+++ b/libs/androidfw/ZipFileRO.cpp
@@ -119,30 +119,41 @@
  * appear to be bogus.
  */
 bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
+ uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
+ uint32_t* pModWhen, uint32_t* pCrc32) const
+{
+     return getEntryInfo(entry, pMethod, pUncompLen, pCompLen, pOffset, pModWhen,
+      pCrc32, nullptr);
+}
+
+bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
     uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
-    uint32_t* pModWhen, uint32_t* pCrc32) const
+    uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const
 {
     const _ZipEntryRO* zipEntry = reinterpret_cast<_ZipEntryRO*>(entry);
     const ZipEntry& ze = zipEntry->entry;
 
-    if (pMethod != NULL) {
+    if (pMethod != nullptr) {
         *pMethod = ze.method;
     }
-    if (pUncompLen != NULL) {
+    if (pUncompLen != nullptr) {
         *pUncompLen = ze.uncompressed_length;
     }
-    if (pCompLen != NULL) {
+    if (pCompLen != nullptr) {
         *pCompLen = ze.compressed_length;
     }
-    if (pOffset != NULL) {
+    if (pOffset != nullptr) {
         *pOffset = ze.offset;
     }
-    if (pModWhen != NULL) {
+    if (pModWhen != nullptr) {
         *pModWhen = ze.mod_time;
     }
-    if (pCrc32 != NULL) {
+    if (pCrc32 != nullptr) {
         *pCrc32 = ze.crc32;
     }
+    if (pExtraFieldSize != nullptr) {
+        *pExtraFieldSize = ze.extra_field_size;
+    }
 
     return true;
 }
diff --git a/libs/androidfw/include/androidfw/ZipFileRO.h b/libs/androidfw/include/androidfw/ZipFileRO.h
index 031d2e8..f7c5007 100644
--- a/libs/androidfw/include/androidfw/ZipFileRO.h
+++ b/libs/androidfw/include/androidfw/ZipFileRO.h
@@ -151,6 +151,10 @@
         uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen,
         uint32_t* pCrc32) const;
 
+    bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
+        uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
+        uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const;
+
     /*
      * Create a new FileMap object that maps a subset of the archive. For
      * an uncompressed entry this effectively provides a pointer to the
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 9fd386f..b6b1a45 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -68,14 +68,6 @@
   <string name="choose_create_option_password_title">Save password to sign in to <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string>
   <!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is others. [CHAR LIMIT=200] -->
   <string name="choose_create_option_sign_in_title">Save sign-in info for <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string>
-  <!-- This appears as a description of the modal bottom sheet when the single tap sign in flow is used for the create passkey flow. [CHAR LIMIT=200] -->
-  <string name="choose_create_single_tap_passkey_title">Use your screen lock to create a passkey for <xliff:g id="app_name" example="Shrine">%1$s</xliff:g>?</string>
-  <!-- This appears as a description of the modal bottom sheet when the single tap sign in flow is used for the create password flow. [CHAR LIMIT=200] -->
-  <string name="choose_create_single_tap_password_title">Use your screen lock to create a password for <xliff:g id="app_name" example="Shrine">%1$s</xliff:g>?</string>
-  <!-- This appears as a description of the modal bottom sheet when the single tap sign in flow is used for the create flow when the credential type is others. [CHAR LIMIT=200] -->
-  <!-- TODO(b/326243891) : Confirm with team on dynamically setting this based on recent product and ux discussions (does not disrupt e2e) -->
-  <string name="choose_create_single_tap_sign_in_title">Use your screen lock to save sign in info for <xliff:g id="app_name" example="Shrine">%1$s</xliff:g>?</string>
-  <!-- Types which are inserted as a placeholder as credentialTypes for other strings. [CHAR LIMIT=200] -->
   <string name="passkey">passkey</string>
   <string name="password">password</string>
   <string name="passkeys">passkeys</string>
@@ -133,6 +125,12 @@
   <string name="get_dialog_title_single_tap_for">Use your screen lock to sign in to <xliff:g id="app_name" example="Shrine">%1$s</xliff:g> with <xliff:g id="username" example="beckett-bakery@gmail.com">%2$s</xliff:g></string>
   <!-- This appears as the title of the dialog asking for user confirmation to use the single user credential (previously saved or to be created) to sign in to the app. [CHAR LIMIT=200] -->
   <string name="get_dialog_title_use_sign_in_for">Use your sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
+  <!-- This appears as the description of the modal bottom sheet asking for user confirmation to use the biometric screen embedded within credential manager for passkey authentication. [CHAR LIMIT=200] -->
+  <string name="get_dialog_description_single_tap_passkey">Sign in to <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> with your saved passkey for <xliff:g id="username" example="beckett@gmail.com">%2$s</xliff:g>.</string>
+  <!-- This appears as the description of the modal bottom sheet asking for user confirmation to use the biometric screen embedded within credential manager for password authentication. [CHAR LIMIT=200] -->
+  <string name="get_dialog_description_single_tap_password">Sign in to <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> with your saved password for <xliff:g id="username" example="beckett@gmail.com">%2$s</xliff:g>.</string>
+  <!-- This appears as the description of the modal bottom sheet asking for user confirmation to use the biometric screen embedded within credential manager for saved sign-in authentication. [CHAR LIMIT=200] -->
+  <string name="get_dialog_description_single_tap_saved_sign_in">Sign in to <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> with your saved sign-in info for <xliff:g id="username" example="beckett@gmail.com">%2$s</xliff:g>.</string>
   <!-- This appears as the title of the dialog asking for user confirmation to unlock / authenticate (e.g. via fingerprint, faceId, passcode etc.) so that we can retrieve their sign-in options. [CHAR LIMIT=200] -->
   <string name="get_dialog_title_unlock_options_for">Unlock sign-in options for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
   <!-- This appears as the title of the dialog asking for user to make a choice from multiple previously saved passkey to sign in to the app. [CHAR LIMIT=200] -->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 36f6ad2..429bdbf 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -270,6 +270,15 @@
         )
     }
 
+    fun getFlowOnMoreOptionOnlySelected() {
+        Log.d(Constants.LOG_TAG, "More Option Only selected")
+        uiState = uiState.copy(
+                getCredentialUiState = uiState.getCredentialUiState?.copy(
+                        currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY
+                )
+        )
+    }
+
     fun getFlowOnMoreOptionOnSnackBarSelected(isNoAccount: Boolean) {
         Log.d(Constants.LOG_TAG, "More Option on snackBar selected")
         uiState = uiState.copy(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
index e088d3a..95f49e9 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
@@ -17,9 +17,12 @@
 package com.android.credentialmanager.common
 
 import android.content.Context
+import android.content.DialogInterface
 import android.graphics.Bitmap
 import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.BiometricManager.Authenticators
 import android.hardware.biometrics.BiometricPrompt
+import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
 import android.os.CancellationSignal
 import android.util.Log
 import androidx.core.content.ContextCompat.getMainExecutor
@@ -43,19 +46,23 @@
  * Namely, this adds the ability to encapsulate the [providerIcon], the providers icon, the
  * [providerName], which represents the name of the provider, the [displayTitleText] which is
  * the large text displaying the flow in progress, and the [descriptionForCredential], which
- * describes details of where the credential is being saved, and how.
- * (E.g. assume a hypothetical provider 'Any Provider' for *passkey* flows with Your@Email.com:
+ * describes details of where the credential is being saved, and how. [displaySubtitleText] is only expected
+ * to be used by the 'create' flow, optionally, and describes the saved name of the creating entity.
+ * (E.g. assume a hypothetical provider 'Any Provider' for *passkey* flows with Your@Email.com and
+ * name 'Your', and an rp called 'The App'):
  *
  * 'get' flow:
  *     - [providerIcon] and [providerName] = 'Any Provider' (and it's icon)
- *     - [displayTitleText] = "Use your saved passkey for Any Provider?"
- *     - [descriptionForCredential] = "Use your screen lock to sign in to Any Provider with
- *     Your@Email.com"
+ *     - [displayTitleText] = "Use your saved passkey for The App?"
+ *     - [descriptionForCredential] = "Sign in to The App with your saved passkey for
+ *     Your@gmail.com"
  *
  * 'create' flow:
  *     - [providerIcon] and [providerName] = 'Any Provider' (and it's icon)
  *     - [displayTitleText] = "Create passkey to sign in to Any Provider?"
- *     - [descriptionForCredential] = "Use your screen lock to create a passkey for Any Provider?"
+ *     - [subtitle] = "Your"
+ *     - [descriptionForCredential] = "You can use your passkey on other devices. It is saved to
+ *  *     Google Password Manager for Your@gmail.com."
  * ).
  *
  * The above are examples; the credential type can change depending on scenario.
@@ -65,8 +72,9 @@
     val providerIcon: Bitmap,
     val providerName: String,
     val displayTitleText: String,
-    val descriptionForCredential: String,
+    val descriptionForCredential: String?,
     val biometricRequestInfo: BiometricRequestInfo,
+    val displaySubtitleText: CharSequence? = null,
 )
 
 /**
@@ -85,7 +93,7 @@
  * so that should this object exist, the result will be retrievable.
  */
 data class BiometricResult(
-    val biometricAuthenticationResult: BiometricPrompt.AuthenticationResult
+    val biometricAuthenticationResult: BiometricPrompt.AuthenticationResult,
 )
 
 /**
@@ -97,15 +105,6 @@
 )
 
 /**
- * Encapsulates the help callback results to easily manage biometric help states in the flow.
- * To specify, this allows us to parse the onAuthenticationHelp method in the [BiometricPrompt].
- */
-data class BiometricHelp(
-    val helpCode: Int,
-    var helpString: CharSequence? = null
-)
-
-/**
  * This is the entry point to start the integrated biometric prompt for 'get' flows. It captures
  * information specific to the get flow, along with required shared callbacks and more general
  * info across both flows, such as the tapped [EntryInfo] or [sendDataToProvider].
@@ -147,7 +146,7 @@
 
     Log.d(TAG, "The BiometricPrompt API call begins.")
     runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
-        onBiometricFailureFallback, BiometricFlowType.GET)
+        onBiometricFailureFallback, BiometricFlowType.GET, onCancelFlowAndFinish)
 }
 
 /**
@@ -191,14 +190,15 @@
 
     Log.d(TAG, "The BiometricPrompt API call begins.")
     runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
-        onBiometricFailureFallback, BiometricFlowType.CREATE)
+        onBiometricFailureFallback, BiometricFlowType.CREATE, onCancelFlowAndFinish)
 }
 
 /**
  * This will handle the logic for integrating credential manager with the biometric prompt for the
  * single account biometric experience. This simultaneously handles both the get and create flows,
  * by retrieving all the data from credential manager, and properly parsing that data into the
- * biometric prompt.
+ * biometric prompt. It will fallback in cases where the biometric api cannot be called, or when
+ * only device credentials are requested.
  */
 private fun runBiometricFlow(
     context: Context,
@@ -206,28 +206,98 @@
     callback: BiometricPrompt.AuthenticationCallback,
     openMoreOptionsPage: () -> Unit,
     onBiometricFailureFallback: (BiometricFlowType) -> Unit,
-    biometricFlowType: BiometricFlowType
+    biometricFlowType: BiometricFlowType,
+    onCancelFlowAndFinish: () -> Unit
 ) {
-    val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage,
-        biometricDisplayInfo.biometricRequestInfo, biometricFlowType)
-
-    val cancellationSignal = CancellationSignal()
-    cancellationSignal.setOnCancelListener {
-        Log.d(TAG, "Your cancellation signal was called.")
-        // TODO(b/333445112) : Migrate towards passing along the developer cancellation signal
-        // or validate the necessity for this
-    }
-
-    val executor = getMainExecutor(context)
-
     try {
-        biometricPrompt.authenticate(cancellationSignal, executor, callback)
-    } catch (e: IllegalArgumentException) {
+        if (onlyUsingDeviceCredentials(biometricDisplayInfo, context)) {
+            onBiometricFailureFallback(biometricFlowType)
+            return
+        }
+
+        val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo,
+            openMoreOptionsPage, biometricDisplayInfo.biometricRequestInfo, onCancelFlowAndFinish)
+
+        val cancellationSignal = CancellationSignal()
+        cancellationSignal.setOnCancelListener {
+            Log.d(TAG, "Your cancellation signal was called.")
+            // TODO(b/333445112) : Migrate towards passing along the developer cancellation signal
+            // or validate the necessity for this
+        }
+
+        val executor = getMainExecutor(context)
+
+        val cryptoOpId = getCryptoOpId(biometricDisplayInfo)
+        if (cryptoOpId != null) {
+            biometricPrompt.authenticate(
+                BiometricPrompt.CryptoObject(cryptoOpId.toLong()),
+                cancellationSignal, executor, callback)
+        } else {
+            biometricPrompt.authenticate(cancellationSignal, executor, callback)
+        }
+    } catch (e: Exception) {
+        // TODO(b/334923201) : Specialize exception catching
         Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n")
         onBiometricFailureFallback(biometricFlowType)
     }
 }
 
+private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Int? {
+    return biometricDisplayInfo.biometricRequestInfo.opId
+}
+
+/**
+ * Determines if, given the allowed authenticators, the flow should fallback early. This has
+ * consistency because for biometrics to exist, **device credentials must exist**. Thus, fallbacks
+ * occur if *only* device credentials are available, to avoid going right into the PIN screen.
+ * Note that if device credential is the only available modality but not requested, or if none
+ * of the requested modalities are available, we propagate the error to the provider instead of
+ * falling back and expect them to handle it as they would prior.
+ * // TODO(b/334197980) : Finalize error propagation/not propagation in real use cases
+ */
+private fun onlyUsingDeviceCredentials(
+    biometricDisplayInfo: BiometricDisplayInfo,
+    context: Context
+): Boolean {
+    val allowedAuthenticators = biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators
+    if (allowedAuthenticators == BiometricManager.Authenticators.DEVICE_CREDENTIAL) {
+        return true
+    }
+
+    val allowedAuthContainsDeviceCredential = containsBiometricAuthenticatorWithDeviceCredentials(
+        allowedAuthenticators)
+
+    if (!allowedAuthContainsDeviceCredential) {
+        // At this point, allowed authenticators is requesting biometrics without device creds.
+        // Thus, a fallback mechanism will be displayed via our own negative button - "cancel".
+        // Beyond this point, fallbacks will occur if none of the stronger authenticators can
+        // be used.
+        return false
+    }
+
+    val biometricManager = context.getSystemService(Context.BIOMETRIC_SERVICE) as BiometricManager
+
+    if (allowedAuthContainsDeviceCredential &&
+        biometricManager.canAuthenticate(Authenticators.BIOMETRIC_WEAK) !=
+        BiometricManager.BIOMETRIC_SUCCESS &&
+        biometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG) !=
+        BiometricManager.BIOMETRIC_SUCCESS) {
+        return true
+    }
+
+    return false
+}
+
+private fun containsBiometricAuthenticatorWithDeviceCredentials(
+    allowedAuthenticators: Int
+): Boolean {
+    val allowedAuthContainsDeviceCredential = (allowedAuthenticators ==
+            Authenticators.BIOMETRIC_WEAK or Authenticators.DEVICE_CREDENTIAL) ||
+            (allowedAuthenticators ==
+                    Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL)
+    return allowedAuthContainsDeviceCredential
+}
+
 /**
  * Sets up the biometric prompt with the UI specific bits.
  * // TODO(b/333445112) : Pass in opId once dependency is confirmed via CryptoObject
@@ -237,49 +307,34 @@
     biometricDisplayInfo: BiometricDisplayInfo,
     openMoreOptionsPage: () -> Unit,
     biometricRequestInfo: BiometricRequestInfo,
-    biometricFlowType: BiometricFlowType,
+    onCancelFlowAndFinish: () -> Unit
 ): BiometricPrompt {
-    val finalAuthenticators = removeDeviceCredential(biometricRequestInfo.allowedAuthenticators)
+    val listener =
+        DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> openMoreOptionsPage() }
 
-    val biometricPrompt = BiometricPrompt.Builder(context)
+    val promptContentViewBuilder = PromptContentViewWithMoreOptionsButton.Builder()
+        .setMoreOptionsButtonListener(context.mainExecutor, listener)
+    biometricDisplayInfo.descriptionForCredential?.let {
+        promptContentViewBuilder.setDescription(it) }
+
+    val biometricPromptBuilder = BiometricPrompt.Builder(context)
         .setTitle(biometricDisplayInfo.displayTitleText)
-        // TODO(b/333445112) : Migrate to using new methods and strings recently aligned upon
-        .setNegativeButton(context.getString(if (biometricFlowType == BiometricFlowType.GET)
-            R.string
-                .dropdown_presentation_more_sign_in_options_text else R.string.string_more_options),
-            getMainExecutor(context)) { _, _ ->
-            openMoreOptionsPage()
-        }
-        .setAllowedAuthenticators(finalAuthenticators)
+        .setAllowedAuthenticators(biometricRequestInfo.allowedAuthenticators)
         .setConfirmationRequired(true)
         .setLogoBitmap(biometricDisplayInfo.providerIcon)
         .setLogoDescription(biometricDisplayInfo.providerName)
-        .setDescription(biometricDisplayInfo.descriptionForCredential)
-        .build()
+        .setContentView(promptContentViewBuilder.build())
 
-    return biometricPrompt
-}
-
-// TODO(b/333445112) : Remove after larger level alignments made on fallback negative button
-// For the time being, we do not support the pin fallback until UX is decided.
-private fun removeDeviceCredential(requestAllowedAuthenticators: Int): Int {
-    var finalAuthenticators = requestAllowedAuthenticators
-
-    if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL or
-                BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
-        finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+    if (!containsBiometricAuthenticatorWithDeviceCredentials(biometricDisplayInfo
+            .biometricRequestInfo.allowedAuthenticators)) {
+        biometricPromptBuilder.setNegativeButton(context.getString(R.string.string_cancel),
+            getMainExecutor(context)
+        ) { _: DialogInterface?, _: Int -> onCancelFlowAndFinish() }
     }
 
-    if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL or
-                BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
-        finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
-    }
+    biometricDisplayInfo.displaySubtitleText?.let { biometricPromptBuilder.setSubtitle(it) }
 
-    if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
-        finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
-    }
-
-    return finalAuthenticators
+    return biometricPromptBuilder.build()
 }
 
 /**
@@ -417,15 +472,29 @@
     }
     val singleEntryType = selectedEntry.credentialType
     val username = selectedEntry.userName
+
+    // TODO(b/330396140) : Finalize localization and parsing for specific sign in option flows
+    //  (fingerprint, face, etc...))
     displayTitleText = context.getString(
         generateDisplayTitleTextResCode(singleEntryType),
         getRequestDisplayInfo.appName
     )
+
     descriptionText = context.getString(
-        R.string.get_dialog_title_single_tap_for,
+        when (singleEntryType) {
+            CredentialType.PASSKEY ->
+                R.string.get_dialog_description_single_tap_passkey
+
+            CredentialType.PASSWORD ->
+                R.string.get_dialog_description_single_tap_password
+
+            CredentialType.UNKNOWN ->
+                R.string.get_dialog_description_single_tap_saved_sign_in
+        },
         getRequestDisplayInfo.appName,
         username
     )
+
     return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
         displayTitleText = displayTitleText, descriptionForCredential = descriptionText,
         biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo)
@@ -451,23 +520,12 @@
         getCreateTitleResCode(createRequestDisplayInfo),
         createRequestDisplayInfo.appName
     )
-    val descriptionText: String = context.getString(
-        when (createRequestDisplayInfo.type) {
-            CredentialType.PASSKEY ->
-                R.string.choose_create_single_tap_passkey_title
 
-            CredentialType.PASSWORD ->
-                R.string.choose_create_single_tap_password_title
-
-            CredentialType.UNKNOWN ->
-                R.string.choose_create_single_tap_sign_in_title
-        },
-        createRequestDisplayInfo.appName,
-    )
-    // TODO(b/333445112) : Add a subtitle and any other recently aligned ideas
+    // TODO(b/330396140) : If footerDescription is null, determine if we need to fallback
     return BiometricDisplayInfo(providerIcon = icon, providerName = providerName,
-        displayTitleText = displayTitleText, descriptionForCredential = descriptionText,
-        biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo)
+        displayTitleText = displayTitleText, descriptionForCredential = selectedEntry
+            .footerDescription, biometricRequestInfo = selectedEntry.biometricRequest
+                as BiometricRequestInfo, displaySubtitleText = createRequestDisplayInfo.title)
 }
 
 /**
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
index d13d86f..149c14a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
@@ -349,6 +349,38 @@
     }
 }
 
+@Composable
+fun MoreOptionTopAppBarWithCustomNavigation(
+        text: String,
+        onNavigationIconClicked: () -> Unit,
+        navigationIcon: ImageVector,
+        navigationIconContentDescription: String,
+        bottomPadding: Dp,
+) {
+    Row(
+            modifier = Modifier.padding(top = 12.dp, bottom = bottomPadding),
+            verticalAlignment = Alignment.CenterVertically,
+    ) {
+        IconButton(
+                modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 4.dp).size(48.dp),
+                onClick = onNavigationIconClicked
+        ) {
+            Box(
+                    modifier = Modifier.size(48.dp),
+                    contentAlignment = Alignment.Center,
+            ) {
+                Icon(
+                        imageVector = navigationIcon,
+                        contentDescription = navigationIconContentDescription,
+                        modifier = Modifier.size(24.dp).autoMirrored(),
+                        tint = LocalAndroidColorScheme.current.onSurfaceVariant,
+                )
+            }
+        }
+        LargeTitleText(text = text, modifier = Modifier.padding(horizontal = 4.dp))
+    }
+}
+
 private fun Modifier.autoMirrored() = composed {
     when (LocalLayoutDirection.current) {
         LayoutDirection.Rtl -> graphicsLayer(scaleX = -1f)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index c1120bb3..e68baf4 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -32,6 +32,7 @@
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.lazy.items
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.outlined.QrCodeScanner
 import androidx.compose.material3.Divider
 import androidx.compose.material3.TextButton
@@ -70,6 +71,7 @@
 import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant
 import com.android.credentialmanager.common.ui.ModalBottomSheet
 import com.android.credentialmanager.common.ui.MoreOptionTopAppBar
+import com.android.credentialmanager.common.ui.MoreOptionTopAppBarWithCustomNavigation
 import com.android.credentialmanager.common.ui.SheetContainerCard
 import com.android.credentialmanager.common.ui.Snackbar
 import com.android.credentialmanager.common.ui.SnackbarActionText
@@ -148,7 +150,7 @@
                                 .currentScreenState == GetScreenState.BIOMETRIC_SELECTION) {
                             BiometricSelectionPage(
                                 biometricEntry = getCredentialUiState.activeEntry,
-                                onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected,
+                                onMoreOptionSelected = viewModel::getFlowOnMoreOptionOnlySelected,
                                 onCancelFlowAndFinish = viewModel::onUserCancel,
                                 onIllegalStateAndFinish = viewModel::onIllegalUiState,
                                 requestDisplayInfo = getCredentialUiState.requestDisplayInfo,
@@ -163,6 +165,28 @@
                                 onBiometricPromptStateChange =
                                 viewModel::onBiometricPromptStateChange
                             )
+                        } else if (credmanBiometricApiEnabled() &&
+                                getCredentialUiState.currentScreenState
+                                == GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY) {
+                            AllSignInOptionCard(
+                                    providerInfoList = getCredentialUiState.providerInfoList,
+                                    providerDisplayInfo = getCredentialUiState.providerDisplayInfo,
+                                    onEntrySelected = viewModel::getFlowOnEntrySelected,
+                                    onBackButtonClicked = viewModel::onUserCancel,
+                                    onCancel = viewModel::onUserCancel,
+                                    onLog = { viewModel.logUiEvent(it) },
+                                    customTopBar = { MoreOptionTopAppBarWithCustomNavigation(
+                                            text = stringResource(
+                                                    R.string.get_dialog_title_sign_in_options),
+                                            onNavigationIconClicked = viewModel::onUserCancel,
+                                            navigationIcon = Icons.Filled.Close,
+                                            navigationIconContentDescription =
+                                            stringResource(R.string.accessibility_close_button),
+                                            bottomPadding = 0.dp
+                                    ) }
+                            )
+                            viewModel.uiMetrics.log(GetCredentialEvent
+                                    .CREDMAN_GET_CRED_SCREEN_ALL_SIGN_IN_OPTIONS)
                         } else {
                             AllSignInOptionCard(
                                 providerInfoList = getCredentialUiState.providerInfoList,
@@ -642,7 +666,13 @@
     return providerId
 }
 
-/** Draws the secondary credential selection page, where all sign-in options are listed. */
+/**
+ * Draws the secondary credential selection page, where all sign-in options are listed.
+ *
+ * By default, this card has 'back' navigation whereby user can navigate back to invoke
+ * [onBackButtonClicked]. However if a different top bar with possibly a different navigation
+ * is required, then the caller of this Composable can set a [customTopBar].
+ */
 @Composable
 fun AllSignInOptionCard(
     providerInfoList: List<ProviderInfo>,
@@ -651,16 +681,21 @@
     onBackButtonClicked: () -> Unit,
     onCancel: () -> Unit,
     onLog: @Composable (UiEventEnum) -> Unit,
+    customTopBar: (@Composable() () -> Unit)? = null
 ) {
     val sortedUserNameToCredentialEntryList =
         providerDisplayInfo.sortedUserNameToCredentialEntryList
     val authenticationEntryList = providerDisplayInfo.authenticationEntryList
     SheetContainerCard(topAppBar = {
-        MoreOptionTopAppBar(
-            text = stringResource(R.string.get_dialog_title_sign_in_options),
-            onNavigationIconClicked = onBackButtonClicked,
-            bottomPadding = 0.dp,
-        )
+        if (customTopBar != null) {
+            customTopBar()
+        } else {
+            MoreOptionTopAppBar(
+                    text = stringResource(R.string.get_dialog_title_sign_in_options),
+                    onNavigationIconClicked = onBackButtonClicked,
+                    bottomPadding = 0.dp,
+            )
+        }
     }) {
         var isFirstSection = true
         // For username
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index b03407b..8e78861 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -163,7 +163,11 @@
     /** The single tap biometric selection page. */
     BIOMETRIC_SELECTION,
 
-    /** The secondary credential selection page, where all sign-in options are listed. */
+    /**
+     * The secondary credential selection page, where all sign-in options are listed.
+     *
+     * This state is expected to go back to PRIMARY_SELECTION on back navigation
+     */
     ALL_SIGN_IN_OPTIONS,
 
     /** The snackbar only page when there's no account but only a remoteEntry. */
@@ -171,6 +175,14 @@
 
     /** The snackbar when there are only auth entries and all of them turn out to be empty. */
     UNLOCKED_AUTH_ENTRIES_ONLY,
+
+    /**
+     * The secondary credential selection page, where all sign-in options are listed.
+     *
+     * This state has no option for the user to navigate back to PRIMARY_SELECTION, and
+     * instead can be terminated independently.
+     */
+    ALL_SIGN_IN_OPTIONS_ONLY,
 }
 
 
@@ -285,7 +297,7 @@
         providerDisplayInfo.remoteEntry != null)
         GetScreenState.REMOTE_ONLY
     else if (isRequestForAllOptions)
-        GetScreenState.ALL_SIGN_IN_OPTIONS
+        GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY
     else if (isBiometricFlow(providerDisplayInfo, isFlowAutoSelectable(providerDisplayInfo)))
         GetScreenState.BIOMETRIC_SELECTION
     else GetScreenState.PRIMARY_SELECTION
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 40db52e..c88c373 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -117,6 +117,7 @@
         "SystemUILogLib",
         "SystemUIPluginLib",
         "SystemUISharedLib",
+        "SystemUI-shared-utils",
         "SystemUI-statsd",
         "SettingsLib",
         "com_android_systemui_flags_lib",
@@ -263,6 +264,7 @@
         "SystemUISharedLib",
         "SystemUICustomizationLib",
         "SystemUICustomizationTestUtils",
+        "SystemUI-shared-utils",
         "SystemUI-statsd",
         "SettingsLib",
         "com_android_systemui_flags_lib",
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/UnfoldModifiers.kt b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/UnfoldModifiers.kt
new file mode 100644
index 0000000..c2a2696
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/UnfoldModifiers.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.fold.ui.composable
+
+import androidx.annotation.FloatRange
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.modifiers.padding
+import kotlin.math.roundToInt
+
+/**
+ * Applies a translation that feeds off of the unfold transition that's active while the device is
+ * being folded or unfolded, effectively shifting the element towards the fold hinge.
+ *
+ * @param startSide `true` if the affected element is on the start side (left-hand side in
+ *   left-to-right layouts), `false` otherwise.
+ * @param fullTranslation The maximum translation to apply when the element is the most shifted. The
+ *   modifier will never apply more than this much translation on the element.
+ * @param unfoldProgress A provider for the amount of progress of the unfold transition. This should
+ *   be sourced from the `UnfoldTransitionInteractor`, ideally through a view-model.
+ */
+@Composable
+fun Modifier.unfoldTranslation(
+    startSide: Boolean,
+    fullTranslation: Dp,
+    @FloatRange(from = 0.0, to = 1.0) unfoldProgress: () -> Float,
+): Modifier {
+    val translateToTheRight = startSide && LocalLayoutDirection.current == LayoutDirection.Ltr
+    return this.graphicsLayer {
+        translationX =
+            fullTranslation.toPx() *
+                if (translateToTheRight) {
+                    1 - unfoldProgress()
+                } else {
+                    unfoldProgress() - 1
+                }
+    }
+}
+
+/**
+ * Applies horizontal padding that feeds off of the unfold transition that's active while the device
+ * is being folded or unfolded, effectively "squishing" the element on both sides.
+ *
+ * This is horizontal padding so it's applied on both the start and end sides of the element.
+ *
+ * @param fullPadding The maximum padding to apply when the element is the most padded. The modifier
+ *   will never apply more than this much horizontal padding on the element.
+ * @param unfoldProgress A provider for the amount of progress of the unfold transition. This should
+ *   be sourced from the `UnfoldTransitionInteractor`, ideally through a view-model.
+ */
+@Composable
+fun Modifier.unfoldHorizontalPadding(
+    fullPadding: Dp,
+    @FloatRange(from = 0.0, to = 1.0) unfoldProgress: () -> Float,
+): Modifier {
+    return this.padding(
+        horizontal = { (fullPadding.toPx() * (1 - unfoldProgress())).roundToInt() },
+    )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 9bd6f81..01c27a4d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -65,6 +65,8 @@
 import com.android.compose.modifiers.thenIf
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.fold.ui.composable.unfoldHorizontalPadding
+import com.android.systemui.fold.ui.composable.unfoldTranslation
 import com.android.systemui.media.controls.ui.composable.MediaCarousel
 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
@@ -289,6 +291,7 @@
         remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
     val tileSquishiness by
         animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
+    val unfoldTransitionProgress by viewModel.unfoldTransitionProgress.collectAsState()
 
     val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
     val density = LocalDensity.current
@@ -337,10 +340,23 @@
                 modifier =
                     Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
                         .then(brightnessMirrorShowingModifier)
+                        .unfoldHorizontalPadding(
+                            fullPadding = dimensionResource(R.dimen.notification_side_paddings),
+                        ) {
+                            unfoldTransitionProgress
+                        }
             )
 
             Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
-                Box(modifier = Modifier.weight(1f)) {
+                Box(
+                    modifier =
+                        Modifier.weight(1f).unfoldTranslation(
+                            startSide = true,
+                            fullTranslation = dimensionResource(R.dimen.notification_side_paddings),
+                        ) {
+                            unfoldTransitionProgress
+                        },
+                ) {
                     BrightnessMirror(
                         viewModel = viewModel.brightnessMirrorViewModel,
                         qsSceneAdapter = viewModel.qsSceneAdapter,
@@ -407,7 +423,16 @@
                         Modifier.weight(1f)
                             .fillMaxHeight()
                             .padding(bottom = navBarBottomHeight)
-                            .then(brightnessMirrorShowingModifier),
+                            .then(brightnessMirrorShowingModifier)
+                            .unfoldTranslation(
+                                startSide = false,
+                                fullTranslation =
+                                    dimensionResource(
+                                        R.dimen.notification_side_paddings,
+                                    ),
+                            ) {
+                                unfoldTransitionProgress
+                            },
                 )
             }
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 85774c6..60b48f2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -571,7 +571,7 @@
                 // THEN the view layout is never updated
                 verify(windowManager, never()).updateViewLayout(any(), any())
 
-                // CLEANUPL we hide to end the job that listens for the finishedGoingToSleep signal
+                // CLEANUP we hide to end the job that listens for the finishedGoingToSleep signal
                 controllerOverlay.hide()
             }
         }
@@ -595,7 +595,7 @@
                 controllerOverlay.updateOverlayParams(overlayParams)
 
                 // THEN the view layout is updated
-                verify(windowManager, never()).updateViewLayout(any(), any())
+                verify(windowManager).updateViewLayout(any(), any())
             }
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 470d342..65fd101 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -85,6 +85,7 @@
 import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
 import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
 import com.android.systemui.testKosmos
+import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -228,6 +229,7 @@
                 footerActionsController = kosmos.footerActionsController,
                 footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory,
                 sceneInteractor = sceneInteractor,
+                unfoldTransitionInteractor = kosmos.unfoldTransitionInteractor,
             )
 
         val displayTracker = FakeDisplayTracker(context)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index ab95e2c..2727af6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -44,6 +44,8 @@
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.testKosmos
+import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
+import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -91,6 +93,7 @@
                 footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory,
                 footerActionsController = kosmos.footerActionsController,
                 sceneInteractor = kosmos.sceneInteractor,
+                unfoldTransitionInteractor = kosmos.unfoldTransitionInteractor,
             )
     }
 
@@ -254,4 +257,26 @@
             shadeRepository.setShadeMode(ShadeMode.Split)
             assertThat(shadeMode).isEqualTo(ShadeMode.Split)
         }
+
+    @Test
+    fun unfoldTransitionProgress() =
+        testScope.runTest {
+            val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider
+            val progress by collectLastValue(underTest.unfoldTransitionProgress)
+
+            unfoldProvider.onTransitionStarted()
+            assertThat(progress).isEqualTo(1f)
+
+            repeat(10) { repetition ->
+                val transitionProgress = 0.1f * (repetition + 1)
+                unfoldProvider.onTransitionProgress(transitionProgress)
+                assertThat(progress).isEqualTo(transitionProgress)
+            }
+
+            unfoldProvider.onTransitionFinishing()
+            assertThat(progress).isEqualTo(1f)
+
+            unfoldProvider.onTransitionFinished()
+            assertThat(progress).isEqualTo(1f)
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
similarity index 60%
rename from packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
index 6a801e0..3b4cce4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
@@ -15,42 +15,31 @@
  */
 package com.android.systemui.unfold.domain.interactor
 
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
-import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
 import com.google.common.truth.Truth.assertThat
-import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.async
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
-open class UnfoldTransitionInteractorTest : SysuiTestCase() {
+@RunWith(AndroidJUnit4::class)
+class UnfoldTransitionInteractorTest : SysuiTestCase() {
 
-    private val testScope = TestScope()
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val unfoldTransitionProgressProvider = kosmos.fakeUnfoldTransitionProgressProvider
 
-    private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider()
-    private val unfoldTransitionRepository =
-        UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider))
-
-    private lateinit var underTest: UnfoldTransitionInteractor
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest = UnfoldTransitionInteractorImpl(unfoldTransitionRepository)
-    }
+    private val underTest: UnfoldTransitionInteractor = kosmos.unfoldTransitionInteractor
 
     @Test
     fun waitForTransitionFinish_noEvents_doesNotComplete() =
@@ -88,4 +77,26 @@
             assertThat(deferred.isCompleted).isFalse()
             deferred.cancel()
         }
+
+    @Test
+    fun unfoldProgress() =
+        testScope.runTest {
+            val progress by collectLastValue(underTest.unfoldProgress)
+            runCurrent()
+
+            unfoldTransitionProgressProvider.onTransitionStarted()
+            assertThat(progress).isEqualTo(1f)
+
+            repeat(10) { repetition ->
+                val transitionProgress = 0.1f * (repetition + 1)
+                unfoldTransitionProgressProvider.onTransitionProgress(transitionProgress)
+                assertThat(progress).isEqualTo(transitionProgress)
+            }
+
+            unfoldTransitionProgressProvider.onTransitionFinishing()
+            assertThat(progress).isEqualTo(1f)
+
+            unfoldTransitionProgressProvider.onTransitionFinished()
+            assertThat(progress).isEqualTo(1f)
+        }
 }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 8e2bd9b..79bf5f1 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -267,6 +267,9 @@
 
     /** True if the clock will react to tone changes in the seed color. */
     val isReactiveToTone: Boolean = true,
+
+    /** True if the clock is large frame clock, which will use weather in compose. */
+    val useCustomClockScene: Boolean = false,
 )
 
 /** Render configuration options for a clock face. Modifies the way SystemUI behaves. */
@@ -283,6 +286,9 @@
      * animation will be used (e.g. a simple translation).
      */
     val hasCustomPositionUpdatedAnimation: Boolean = false,
+
+    /** True if the clock is large frame clock, which will use weatherBlueprint in compose. */
+    val useCustomClockScene: Boolean = false,
 )
 
 /** Structure for keeping clock-specific settings */
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 61d1c71..4a60d19 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -323,7 +323,7 @@
         overlayParams = updatedOverlayParams
         sensorBounds = updatedOverlayParams.sensorBounds
         getTouchOverlay()?.let {
-            if (addViewRunnable != null) {
+            if (addViewRunnable == null) {
                 // Only updateViewLayout if there's no pending view to add to WM.
                 // If there is a pending view, that means the view hasn't been added yet so there's
                 // no need to update any layouts. Instead the correct params will be used when the
diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt
index d4a1f74..0c181e9 100644
--- a/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt
@@ -16,14 +16,14 @@
 
 package com.android.systemui.common.coroutine
 
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow as wrapped
 import kotlin.experimental.ExperimentalTypeInference
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.channels.ProducerScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.buffer
 import kotlinx.coroutines.flow.callbackFlow
 
+@Deprecated("Use com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow instead")
 object ConflatedCallbackFlow {
 
     /**
@@ -32,9 +32,15 @@
      * consumer(s) of the values in the flow), the values are buffered and, if the buffer fills up,
      * we drop the oldest values automatically instead of suspending the producer.
      */
-    @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-    @OptIn(ExperimentalTypeInference::class, ExperimentalCoroutinesApi::class)
+    @Deprecated(
+        "Use com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow instead",
+        ReplaceWith(
+            "conflatedCallbackFlow",
+            "com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow"
+        )
+    )
+    @OptIn(ExperimentalTypeInference::class)
     fun <T> conflatedCallbackFlow(
         @BuilderInference block: suspend ProducerScope<T>.() -> Unit,
-    ): Flow<T> = callbackFlow(block).buffer(capacity = Channel.CONFLATED)
+    ): Flow<T> = wrapped(block)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 165de7c..21af0a0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -1373,7 +1373,7 @@
     private final Lazy<DreamViewModel> mDreamViewModel;
     private final Lazy<CommunalTransitionViewModel> mCommunalTransitionViewModel;
     private RemoteAnimationTarget mRemoteAnimationTarget;
-    private Boolean mShowCommunalByDefault;
+    private boolean mShowCommunalByDefault = false;
 
     private final Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager;
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
index 80e94a2..20b7b2a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
@@ -23,12 +23,14 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
+import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.kotlin.toPx
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
 
 /**
  * Distance over which the surface behind the keyguard is animated in during a Y-translation
@@ -96,13 +98,21 @@
             .distinctUntilChanged()
 
     /**
+     * Whether a notification launch animation is running when we're not already in the GONE state.
+     */
+    private val isNotificationLaunchAnimationRunningOnKeyguard =
+        notificationLaunchInteractor.isLaunchAnimationRunning
+            .sample(transitionInteractor.finishedKeyguardState)
+            .map { it != KeyguardState.GONE }
+
+    /**
      * Whether we're animating the surface, or a notification launch animation is running (which
      * means we're going to animate the surface, even if animators aren't yet running).
      */
     val isAnimatingSurface =
         combine(
             repository.isAnimatingSurface,
-            notificationLaunchInteractor.isLaunchAnimationRunning
+            isNotificationLaunchAnimationRunningOnKeyguard,
         ) { animatingSurface, animatingLaunch ->
             animatingSurface || animatingLaunch
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 6255f0d..7178e1b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.clocks.ClockController
-import com.android.systemui.shared.clocks.DEFAULT_CLOCK_ID
 import kotlinx.coroutines.launch
 
 object KeyguardClockViewBinder {
@@ -76,13 +75,13 @@
                 }
                 launch {
                     if (!MigrateClocksToBlueprint.isEnabled) return@launch
-                    viewModel.clockShouldBeCentered.collect { clockShouldBeCentered ->
+                    viewModel.clockShouldBeCentered.collect {
                         viewModel.currentClock.value?.let {
-                            // Weather clock also has hasCustomPositionUpdatedAnimation as true
-                            // TODO(b/323020908): remove ID check
+                            // TODO(b/301502635): remove "!it.config.useCustomClockScene" when
+                            // migrate clocks to blueprint is fully rolled out
                             if (
                                 it.largeClock.config.hasCustomPositionUpdatedAnimation &&
-                                    it.config.id == DEFAULT_CLOCK_ID
+                                    !it.config.useCustomClockScene
                             ) {
                                 blueprintInteractor.refreshBlueprint(Type.DefaultClockStepping)
                             } else {
@@ -93,12 +92,9 @@
                 }
                 launch {
                     if (!MigrateClocksToBlueprint.isEnabled) return@launch
-                    viewModel.isAodIconsVisible.collect { isAodIconsVisible ->
+                    viewModel.isAodIconsVisible.collect {
                         viewModel.currentClock.value?.let {
-                            // Weather clock also has hasCustomPositionUpdatedAnimation as true
-                            if (
-                                viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
-                            ) {
+                            if (viewModel.useLargeClock && it.config.useCustomClockScene) {
                                 blueprintInteractor.refreshBlueprint(Type.DefaultTransition)
                             }
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index f6da033..a6d3312 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -118,8 +118,7 @@
                 currentClock
             ) { isLargeClockVisible, clockShouldBeCentered, shadeMode, currentClock ->
                 val shouldUseSplitShade = shadeMode == ShadeMode.Split
-                // TODO(b/326098079): make id a constant field in config
-                if (currentClock?.config?.id == "DIGITAL_CLOCK_WEATHER") {
+                if (currentClock?.config?.useCustomClockScene == true) {
                     val weatherClockLayout =
                         when {
                             shouldUseSplitShade && clockShouldBeCentered ->
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
index e8d3274..5432a18 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
@@ -69,7 +69,11 @@
     private val mediaFlags: MediaFlags,
     private val mediaFilterRepository: MediaFilterRepository,
 ) : MediaDataManager.Listener {
-    lateinit var mediaDataManager: MediaDataManager
+    /** Non-UI listeners to media changes. */
+    private val _listeners: MutableSet<MediaDataProcessor.Listener> = mutableSetOf()
+    val listeners: Set<MediaDataProcessor.Listener>
+        get() = _listeners.toSet()
+    lateinit var mediaDataProcessor: MediaDataProcessor
 
     // Ensure the field (and associated reference) isn't removed during optimization.
     @KeepForWeakReference
@@ -113,6 +117,9 @@
         mediaFilterRepository.addMediaDataLoadingState(
             MediaDataLoadingModel.Loaded(data.instanceId)
         )
+
+        // Notify listeners
+        listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
     }
 
     override fun onSmartspaceMediaDataLoaded(
@@ -171,6 +178,20 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(lastActiveId)
                 )
+                listeners.forEach { listener ->
+                    getKey(lastActiveId)?.let { lastActiveKey ->
+                        listener.onMediaDataLoaded(
+                            lastActiveKey,
+                            lastActiveKey,
+                            mediaData,
+                            receivedSmartspaceCardLatency =
+                                (systemClock.currentTimeMillis() -
+                                        data.headphoneConnectionTimeMillis)
+                                    .toInt(),
+                            isSsReactivated = true
+                        )
+                    }
+                }
             }
         } else if (data.isActive) {
             // Mark to prioritize Smartspace card if no recent media.
@@ -189,6 +210,7 @@
         mediaFilterRepository.setRecommendationsLoadingState(
             SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable)
         )
+        listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
     }
 
     override fun onMediaDataRemoved(key: String) {
@@ -198,6 +220,8 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Removed(instanceId)
                 )
+                // Only notify listeners if something actually changed
+                listeners.forEach { it.onMediaDataRemoved(key) }
             }
         }
     }
@@ -212,6 +236,11 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(lastActiveId, immediately)
                 )
+                listeners.forEach { listener ->
+                    getKey(lastActiveId)?.let { lastActiveKey ->
+                        listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, it, immediately)
+                    }
+                }
             }
         }
 
@@ -227,6 +256,7 @@
         mediaFilterRepository.setRecommendationsLoadingState(
             SmartspaceMediaLoadingModel.Removed(key, immediately)
         )
+        listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
     }
 
     @VisibleForTesting
@@ -240,6 +270,7 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Removed(data.instanceId)
                 )
+                listeners.forEach { listener -> listener.onMediaDataRemoved(key) }
             }
         }
     }
@@ -247,6 +278,7 @@
     @VisibleForTesting
     internal fun handleUserSwitched() {
         // If the user changes, remove all current MediaData objects.
+        val listenersCopy = listeners
         val keyCopy = mediaFilterRepository.selectedUserEntries.value.keys.toMutableList()
         // Clear the list first and update loading state to remove media from UI.
         mediaFilterRepository.clearSelectedUserMedia()
@@ -255,6 +287,9 @@
             mediaFilterRepository.addMediaDataLoadingState(
                 MediaDataLoadingModel.Removed(instanceId)
             )
+            getKey(instanceId)?.let {
+                listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
+            }
         }
 
         mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
@@ -268,6 +303,7 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(data.instanceId)
                 )
+                listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
             }
         }
     }
@@ -279,7 +315,7 @@
         mediaEntries.forEach { (key, data) ->
             if (mediaFilterRepository.selectedUserEntries.value.containsKey(data.instanceId)) {
                 // Force updates to listeners, needed for re-activated card
-                mediaDataManager.setInactive(key, timedOut = true, forceUpdate = true)
+                mediaDataProcessor.setInactive(key, timedOut = true, forceUpdate = true)
             }
         }
         val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
@@ -301,7 +337,7 @@
 
             if (mediaFlags.isPersistentSsCardEnabled()) {
                 mediaFilterRepository.setRecommendation(smartspaceMediaData.copy(isActive = false))
-                mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId)
+                mediaDataProcessor.setRecommendationInactive(smartspaceMediaData.targetId)
             } else {
                 mediaFilterRepository.setRecommendation(
                     EMPTY_SMARTSPACE_MEDIA_DATA.copy(
@@ -309,7 +345,7 @@
                         instanceId = smartspaceMediaData.instanceId,
                     )
                 )
-                mediaDataManager.dismissSmartspaceRecommendation(
+                mediaDataProcessor.dismissSmartspaceRecommendation(
                     smartspaceMediaData.targetId,
                     delay = 0L,
                 )
@@ -317,6 +353,12 @@
         }
     }
 
+    /** Add a listener for filtered [MediaData] changes */
+    fun addListener(listener: MediaDataProcessor.Listener) = _listeners.add(listener)
+
+    /** Remove a listener that was registered with addListener */
+    fun removeListener(listener: MediaDataProcessor.Listener) = _listeners.remove(listener)
+
     /**
      * Return the time since last active for the most-recent media.
      *
@@ -336,6 +378,16 @@
         return sortedEntries[lastActiveInstanceId]?.let { now - it.lastActive } ?: Long.MAX_VALUE
     }
 
+    private fun getKey(instanceId: InstanceId): String? {
+        val allEntries = mediaFilterRepository.allUserEntries.value
+        val filteredEntries = allEntries.filter { (_, data) -> data.instanceId == instanceId }
+        return if (filteredEntries.isNotEmpty()) {
+            filteredEntries.keys.first()
+        } else {
+            null
+        }
+    }
+
     companion object {
         /**
          * Maximum age of a media control to re-activate on smartspace signal. If there is no media
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
index c3ba913..b04e938 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.media.controls.shared.model.MediaCommonModel
 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
+import com.android.systemui.media.controls.util.MediaControlsRefactorFlag
 import com.android.systemui.media.controls.util.MediaFlags
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -156,13 +157,19 @@
             mediaDataProcessor.onSessionDestroyed(key)
         }
         mediaResumeListener.setManager(this)
-        mediaDataFilter.mediaDataManager = this
+        mediaDataFilter.mediaDataProcessor = mediaDataProcessor
     }
 
-    override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) {
-        mediaDataProcessor.setInactive(key, timedOut, forceUpdate)
+    override fun addListener(listener: MediaDataManager.Listener) {
+        mediaDataFilter.addListener(listener)
     }
 
+    override fun removeListener(listener: MediaDataManager.Listener) {
+        mediaDataFilter.removeListener(listener)
+    }
+
+    override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) = unsupported
+
     override fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
         mediaDataProcessor.onNotificationAdded(key, sbn)
     }
@@ -207,9 +214,7 @@
         return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay)
     }
 
-    override fun setRecommendationInactive(key: String) {
-        mediaDataProcessor.setRecommendationInactive(key)
-    }
+    override fun setRecommendationInactive(key: String) = unsupported
 
     override fun onNotificationRemoved(key: String) {
         mediaDataProcessor.onNotificationRemoved(key)
@@ -240,4 +245,12 @@
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         mediaDeviceManager.dump(pw)
     }
+
+    companion object {
+        val unsupported: Nothing
+            get() =
+                error(
+                    "Code path not supported when ${MediaControlsRefactorFlag.FLAG_NAME} is enabled"
+                )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
new file mode 100644
index 0000000..14a9179
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.binder
+
+import android.widget.ImageButton
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.res.R
+
+object MediaControlViewBinder {
+
+    fun setVisibleAndAlpha(set: ConstraintSet, resId: Int, visible: Boolean) {
+        setVisibleAndAlpha(set, resId, visible, ConstraintSet.GONE)
+    }
+
+    private fun setVisibleAndAlpha(
+        set: ConstraintSet,
+        resId: Int,
+        visible: Boolean,
+        notVisibleValue: Int
+    ) {
+        set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else notVisibleValue)
+        set.setAlpha(resId, if (visible) 1.0f else 0.0f)
+    }
+
+    fun updateSeekBarVisibility(constraintSet: ConstraintSet, isSeekBarEnabled: Boolean) {
+        if (isSeekBarEnabled) {
+            constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.VISIBLE)
+            constraintSet.setAlpha(R.id.media_progress_bar, 1.0f)
+        } else {
+            constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE)
+            constraintSet.setAlpha(R.id.media_progress_bar, 0.0f)
+        }
+    }
+
+    fun setSemanticButtonVisibleAndAlpha(
+        button: ImageButton,
+        expandedSet: ConstraintSet,
+        collapsedSet: ConstraintSet,
+        visible: Boolean,
+        notVisibleValue: Int,
+        showInCollapsed: Boolean
+    ) {
+        if (notVisibleValue == ConstraintSet.INVISIBLE) {
+            // Since time views should appear instead of buttons.
+            button.isFocusable = visible
+            button.isClickable = visible
+        }
+        setVisibleAndAlpha(expandedSet, button.id, visible, notVisibleValue)
+        setVisibleAndAlpha(collapsedSet, button.id, visible = visible && showInCollapsed)
+    }
+}
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 b315cac..7fced5f8 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
@@ -16,41 +16,73 @@
 
 package com.android.systemui.media.controls.ui.controller
 
+import android.animation.Animator
+import android.animation.AnimatorInflater
+import android.animation.AnimatorSet
 import android.content.Context
 import android.content.res.Configuration
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.drawable.Drawable
+import android.provider.Settings
+import android.view.View
+import android.view.animation.Interpolator
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT
+import com.android.app.animation.Interpolators
 import com.android.app.tracing.traceSection
+import com.android.systemui.Flags
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition
+import com.android.systemui.media.controls.ui.animation.MetadataAnimationHandler
+import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder
+import com.android.systemui.media.controls.ui.binder.SeekBarObserver
 import com.android.systemui.media.controls.ui.controller.MediaCarouselController.Companion.calculateAlpha
 import com.android.systemui.media.controls.ui.view.GutsViewHolder
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.media.controls.ui.view.MediaViewHolder
 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder
+import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel
+import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.surfaceeffects.PaintDrawCallback
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView
+import com.android.systemui.surfaceeffects.ripple.MultiRippleController
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
 import com.android.systemui.util.animation.MeasurementInput
 import com.android.systemui.util.animation.MeasurementOutput
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionLayoutController
 import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.settings.GlobalSettings
 import java.lang.Float.max
 import java.lang.Float.min
+import java.util.Random
 import javax.inject.Inject
 
 /**
  * A class responsible for controlling a single instance of a media player handling interactions
  * with the view instance and keeping the media view states up to date.
  */
-class MediaViewController
+open class MediaViewController
 @Inject
 constructor(
     private val context: Context,
     private val configurationController: ConfigurationController,
     private val mediaHostStatesManager: MediaHostStatesManager,
     private val logger: MediaViewLogger,
+    private val seekBarViewModel: SeekBarViewModel,
+    @Main private val mainExecutor: DelayableExecutor,
     private val mediaFlags: MediaFlags,
+    private val globalSettings: GlobalSettings,
 ) {
 
     /**
@@ -131,6 +163,72 @@
             return transitionLayout?.translationY ?: 0.0f
         }
 
+    /** Whether artwork is bound. */
+    var isArtworkBound: Boolean = false
+
+    /** previous background artwork */
+    var prevArtwork: Drawable? = null
+
+    /** Whether scrubbing time can show */
+    var canShowScrubbingTime: Boolean = false
+
+    /** Whether user is touching the seek bar to change the position */
+    var isScrubbing: Boolean = false
+
+    var isSeekBarEnabled: Boolean = false
+
+    /** Not visible value for previous button when scrubbing */
+    private var prevNotVisibleValue = ConstraintSet.GONE
+    private var isPrevButtonAvailable = false
+
+    /** Not visible value for next button when scrubbing */
+    private var nextNotVisibleValue = ConstraintSet.GONE
+    private var isNextButtonAvailable = false
+
+    private lateinit var mediaViewHolder: MediaViewHolder
+    private lateinit var seekBarObserver: SeekBarObserver
+    private lateinit var turbulenceNoiseController: TurbulenceNoiseController
+    private lateinit var loadingEffect: LoadingEffect
+    private lateinit var turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig
+    private lateinit var noiseDrawCallback: PaintDrawCallback
+    private lateinit var stateChangedCallback: LoadingEffect.AnimationStateChangedCallback
+    internal lateinit var metadataAnimationHandler: MetadataAnimationHandler
+    internal lateinit var colorSchemeTransition: ColorSchemeTransition
+    internal lateinit var multiRippleController: MultiRippleController
+
+    private val scrubbingChangeListener =
+        object : SeekBarViewModel.ScrubbingChangeListener {
+            override fun onScrubbingChanged(scrubbing: Boolean) {
+                if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+                if (isScrubbing == scrubbing) return
+                isScrubbing = scrubbing
+                updateDisplayForScrubbingChange()
+            }
+        }
+
+    private val enabledChangeListener =
+        object : SeekBarViewModel.EnabledChangeListener {
+            override fun onEnabledChanged(enabled: Boolean) {
+                if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+                if (isSeekBarEnabled == enabled) return
+                isSeekBarEnabled = enabled
+                MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled)
+            }
+        }
+
+    /**
+     * Sets the listening state of the player.
+     *
+     * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
+     * unnecessary work when the QS panel is closed.
+     *
+     * @param listening True when player should be active. Otherwise, false.
+     */
+    fun setListening(listening: Boolean) {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        seekBarViewModel.listening = listening
+    }
+
     /** A callback for config changes */
     private val configurationListener =
         object : ConfigurationController.ConfigurationListener {
@@ -232,6 +330,14 @@
      * Notify this controller that the view has been removed and all listeners should be destroyed
      */
     fun onDestroy() {
+        if (mediaFlags.isMediaControlsRefactorEnabled()) {
+            if (this::seekBarObserver.isInitialized) {
+                seekBarViewModel.progress.removeObserver(seekBarObserver)
+            }
+            seekBarViewModel.removeScrubbingChangeListener(scrubbingChangeListener)
+            seekBarViewModel.removeEnabledChangeListener(enabledChangeListener)
+            seekBarViewModel.onDestroy()
+        }
         mediaHostStatesManager.removeController(this)
         configurationController.removeCallback(configurationListener)
     }
@@ -546,6 +652,178 @@
             )
         }
 
+    fun attachPlayer(mediaViewHolder: MediaViewHolder) {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        this.mediaViewHolder = mediaViewHolder
+
+        // Setting up seek bar.
+        seekBarObserver = SeekBarObserver(mediaViewHolder)
+        seekBarViewModel.progress.observeForever(seekBarObserver)
+        seekBarViewModel.attachTouchHandlers(mediaViewHolder.seekBar)
+        seekBarViewModel.setScrubbingChangeListener(scrubbingChangeListener)
+        seekBarViewModel.setEnabledChangeListener(enabledChangeListener)
+
+        val mediaCard = mediaViewHolder.player
+        attach(mediaViewHolder.player, TYPE.PLAYER)
+
+        val turbulenceNoiseView = mediaViewHolder.turbulenceNoiseView
+        turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)
+
+        multiRippleController = MultiRippleController(mediaViewHolder.multiRippleView)
+
+        // Metadata Animation
+        val titleText = mediaViewHolder.titleText
+        val artistText = mediaViewHolder.artistText
+        val explicitIndicator = mediaViewHolder.explicitIndicator
+        val enter =
+            loadAnimator(
+                mediaCard.context,
+                R.anim.media_metadata_enter,
+                Interpolators.EMPHASIZED_DECELERATE,
+                titleText,
+                artistText,
+                explicitIndicator
+            )
+        val exit =
+            loadAnimator(
+                mediaCard.context,
+                R.anim.media_metadata_exit,
+                Interpolators.EMPHASIZED_ACCELERATE,
+                titleText,
+                artistText,
+                explicitIndicator
+            )
+        metadataAnimationHandler = MetadataAnimationHandler(exit, enter)
+
+        colorSchemeTransition =
+            ColorSchemeTransition(
+                mediaCard.context,
+                mediaViewHolder,
+                multiRippleController,
+                turbulenceNoiseController
+            )
+
+        // For Turbulence noise.
+        val loadingEffectView = mediaViewHolder.loadingEffectView
+        turbulenceNoiseAnimationConfig =
+            createTurbulenceNoiseConfig(
+                loadingEffectView,
+                turbulenceNoiseView,
+                colorSchemeTransition
+            )
+        noiseDrawCallback =
+            object : PaintDrawCallback {
+                override fun onDraw(paint: Paint) {
+                    loadingEffectView.draw(paint)
+                }
+            }
+        stateChangedCallback =
+            object : LoadingEffect.AnimationStateChangedCallback {
+                override fun onStateChanged(
+                    oldState: LoadingEffect.AnimationState,
+                    newState: LoadingEffect.AnimationState
+                ) {
+                    if (newState === LoadingEffect.AnimationState.NOT_PLAYING) {
+                        loadingEffectView.visibility = View.INVISIBLE
+                    } else {
+                        loadingEffectView.visibility = View.VISIBLE
+                    }
+                }
+            }
+    }
+
+    fun updateAnimatorDurationScale() {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (this::seekBarObserver.isInitialized) {
+            seekBarObserver.animationEnabled =
+                globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f
+        }
+    }
+
+    /** update view with the needed UI changes when user touches seekbar. */
+    private fun updateDisplayForScrubbingChange() {
+        mainExecutor.execute {
+            val isTimeVisible = canShowScrubbingTime && isScrubbing
+            MediaControlViewBinder.setVisibleAndAlpha(
+                expandedLayout,
+                mediaViewHolder.scrubbingTotalTimeView.id,
+                isTimeVisible
+            )
+            MediaControlViewBinder.setVisibleAndAlpha(
+                expandedLayout,
+                mediaViewHolder.scrubbingElapsedTimeView.id,
+                isTimeVisible
+            )
+
+            MediaControlViewModel.SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach { id ->
+                val isButtonVisible: Boolean
+                val notVisibleValue: Int
+                when (id) {
+                    R.id.actionPrev -> {
+                        isButtonVisible = isPrevButtonAvailable && !isTimeVisible
+                        notVisibleValue = prevNotVisibleValue
+                    }
+                    R.id.actionNext -> {
+                        isButtonVisible = isNextButtonAvailable && !isTimeVisible
+                        notVisibleValue = nextNotVisibleValue
+                    }
+                    else -> {
+                        isButtonVisible = !isTimeVisible
+                        notVisibleValue = ConstraintSet.GONE
+                    }
+                }
+                MediaControlViewBinder.setSemanticButtonVisibleAndAlpha(
+                    mediaViewHolder.getAction(id),
+                    expandedLayout,
+                    collapsedLayout,
+                    isButtonVisible,
+                    notVisibleValue,
+                    showInCollapsed = true
+                )
+            }
+
+            if (!metadataAnimationHandler.isRunning) {
+                refreshState()
+            }
+        }
+    }
+
+    fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        seekBarViewModel.logSeek = onSeek
+        onBindSeekBar.invoke(seekBarViewModel)
+    }
+
+    fun setUpTurbulenceNoise() {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (Flags.shaderlibLoadingEffectRefactor()) {
+            if (!this::loadingEffect.isInitialized) {
+                loadingEffect =
+                    LoadingEffect(
+                        TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                        turbulenceNoiseAnimationConfig,
+                        noiseDrawCallback,
+                        stateChangedCallback
+                    )
+            }
+            colorSchemeTransition.loadingEffect = loadingEffect
+            loadingEffect.play()
+            mainExecutor.executeDelayed(
+                loadingEffect::finish,
+                MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION
+            )
+        } else {
+            turbulenceNoiseController.play(
+                TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+                turbulenceNoiseAnimationConfig
+            )
+            mainExecutor.executeDelayed(
+                turbulenceNoiseController::finish,
+                MediaControlViewModel.TURBULENCE_NOISE_PLAY_MS_DURATION
+            )
+        }
+    }
+
     /**
      * Obtain a measurement for a given location. This makes sure that the state is up to date and
      * all widgets know their location. Calling this method may create a measurement if we don't
@@ -801,6 +1079,75 @@
                 applyImmediately = true
             )
         }
+
+    @VisibleForTesting
+    protected open fun loadAnimator(
+        context: Context,
+        animId: Int,
+        motionInterpolator: Interpolator?,
+        vararg targets: View?
+    ): AnimatorSet {
+        val animators = ArrayList<Animator>()
+        for (target in targets) {
+            val animator = AnimatorInflater.loadAnimator(context, animId) as AnimatorSet
+            animator.childAnimations[0].interpolator = motionInterpolator
+            animator.setTarget(target)
+            animators.add(animator)
+        }
+        val result = AnimatorSet()
+        result.playTogether(animators)
+        return result
+    }
+
+    private fun createTurbulenceNoiseConfig(
+        loadingEffectView: LoadingEffectView,
+        turbulenceNoiseView: TurbulenceNoiseView,
+        colorSchemeTransition: ColorSchemeTransition
+    ): TurbulenceNoiseAnimationConfig {
+        val targetView: View =
+            if (Flags.shaderlibLoadingEffectRefactor()) {
+                loadingEffectView
+            } else {
+                turbulenceNoiseView
+            }
+        val width = targetView.width
+        val height = targetView.height
+        val random = Random()
+        return TurbulenceNoiseAnimationConfig(
+            gridCount = 2.14f,
+            TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER,
+            random.nextFloat(),
+            random.nextFloat(),
+            random.nextFloat(),
+            noiseMoveSpeedX = 0.42f,
+            noiseMoveSpeedY = 0f,
+            TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z,
+            // Color will be correctly updated in ColorSchemeTransition.
+            colorSchemeTransition.accentPrimary.currentColor,
+            screenColor = Color.BLACK,
+            width.toFloat(),
+            height.toFloat(),
+            TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS,
+            easeInDuration = 1350f,
+            easeOutDuration = 1350f,
+            targetView.context.resources.displayMetrics.density,
+            lumaMatteBlendFactor = 0.26f,
+            lumaMatteOverallBrightness = 0.09f,
+            shouldInverseNoiseLuminosity = false
+        )
+    }
+
+    fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        isPrevButtonAvailable = isAvailable
+        prevNotVisibleValue = notVisibleValue
+    }
+
+    fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        isNextButtonAvailable = isAvailable
+        nextNotVisibleValue = notVisibleValue
+    }
 }
 
 /** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
index 1e67a77..82099e6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
@@ -24,7 +24,8 @@
     val icon: Drawable?,
     val contentDescription: CharSequence?,
     val background: Drawable?,
-    val isVisible: Boolean = true,
+    /** whether action is visible if user is touching seekbar to change position. */
+    val isVisibleWhenScrubbing: Boolean = true,
     val notVisibleValue: Int = ConstraintSet.GONE,
     val showInCollapsed: Boolean,
     val rebindId: Int? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
index 7c59995..d74506d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.pm.PackageManager
+import android.media.session.MediaController
 import android.media.session.MediaSession.Token
 import android.text.TextUtils
 import android.util.Log
@@ -40,6 +41,7 @@
 import com.android.systemui.monet.Style
 import com.android.systemui.res.R
 import com.android.systemui.util.kotlin.sample
+import java.util.concurrent.Executor
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -51,6 +53,7 @@
 class MediaControlViewModel(
     @Application private val applicationContext: Context,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Background private val backgroundExecutor: Executor,
     private val interactor: MediaControlInteractor,
     private val logger: MediaUiEventLogger,
 ) {
@@ -124,13 +127,15 @@
                 }
             },
             backgroundCover = model.artwork,
-            appIcon = getAppIcon(model.appIcon, model.isResume, model.packageName),
+            appIcon = model.appIcon,
+            launcherIcon = getIconFromApp(model.packageName),
             useGrayColorFilter = model.appIcon == null || model.isResume,
             artistName = model.artistName ?: "",
             titleName = model.songName ?: "",
             isExplicitVisible = model.showExplicit,
+            shouldAddGradient = wallpaperColors != null,
             colorScheme = scheme,
-            isTimeVisible = canShowScrubbingTimeViews(model.semanticActionButtons),
+            canShowTime = canShowScrubbingTimeViews(model.semanticActionButtons),
             playTurbulenceNoise = playTurbulenceNoise,
             useSemanticActions = model.semanticActionButtons != null,
             actionButtons = toActionViewModels(model),
@@ -146,6 +151,21 @@
             onLongClicked = {
                 logger.logLongPressOpen(model.uid, model.packageName, model.instanceId)
             },
+            onSeek = {
+                logger.logSeek(model.uid, model.packageName, model.instanceId)
+                // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_CLICK_EVENT)
+            },
+            onBindSeekbar = { seekBarViewModel ->
+                if (model.isResume && model.resumeProgress != null) {
+                    seekBarViewModel.updateStaticProgress(model.resumeProgress)
+                } else {
+                    backgroundExecutor.execute {
+                        seekBarViewModel.updateController(
+                            model.token?.let { MediaController(applicationContext, it) }
+                        )
+                    }
+                }
+            }
         )
     }
 
@@ -278,16 +298,16 @@
         model: MediaControlModel,
         mediaAction: MediaAction,
         buttonId: Int,
-        isScrubbingTimeEnabled: Boolean
+        canShowScrubbingTimeViews: Boolean
     ): MediaActionViewModel {
         val showInCollapsed = SEMANTIC_ACTIONS_COMPACT.contains(buttonId)
         val hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId)
-        val shouldHideDueToScrubbing = isScrubbingTimeEnabled && hideWhenScrubbing
+        val shouldHideWhenScrubbing = canShowScrubbingTimeViews && hideWhenScrubbing
         return MediaActionViewModel(
             icon = mediaAction.icon,
             contentDescription = mediaAction.contentDescription,
             background = mediaAction.background,
-            isVisible = !shouldHideDueToScrubbing,
+            isVisibleWhenScrubbing = !shouldHideWhenScrubbing,
             notVisibleValue =
                 if (
                     (buttonId == R.id.actionPrev && model.semanticActionButtons!!.reservePrev) ||
@@ -342,19 +362,6 @@
         action.run()
     }
 
-    private fun getAppIcon(
-        icon: android.graphics.drawable.Icon?,
-        isResume: Boolean,
-        packageName: String
-    ): Icon {
-        if (icon != null && !isResume) {
-            icon.loadDrawable(applicationContext)?.let { drawable ->
-                return Icon.Loaded(drawable, null)
-            }
-        }
-        return getIconFromApp(packageName)
-    }
-
     private fun getIconFromApp(packageName: String): Icon {
         return try {
             Icon.Loaded(applicationContext.packageManager.getApplicationIcon(packageName), null)
@@ -381,17 +388,17 @@
         private const val DISABLED_ALPHA = 0.38f
 
         /** Buttons to show in small player when using semantic actions */
-        private val SEMANTIC_ACTIONS_COMPACT =
+        val SEMANTIC_ACTIONS_COMPACT =
             listOf(R.id.actionPlayPause, R.id.actionPrev, R.id.actionNext)
 
         /**
          * Buttons that should get hidden when we are scrubbing (they will be replaced with the
          * views showing scrubbing time)
          */
-        private val SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = listOf(R.id.actionPrev, R.id.actionNext)
+        val SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = listOf(R.id.actionPrev, R.id.actionNext)
 
         /** Buttons to show in player when using semantic actions. */
-        private val SEMANTIC_ACTIONS_ALL =
+        val SEMANTIC_ACTIONS_ALL =
             listOf(
                 R.id.actionPlayPause,
                 R.id.actionPrev,
@@ -399,5 +406,9 @@
                 R.id.action0,
                 R.id.action1
             )
+
+        const val TURBULENCE_NOISE_PLAY_MS_DURATION = 7500L
+        const val MEDIA_PLAYER_SCRIM_START_ALPHA = 0.25f
+        const val MEDIA_PLAYER_SCRIM_END_ALPHA = 1.0f
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
index 9029a65..d1014e8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
@@ -24,13 +24,15 @@
 data class MediaPlayerViewModel(
     val contentDescription: (Boolean) -> CharSequence,
     val backgroundCover: android.graphics.drawable.Icon?,
-    val appIcon: Icon,
+    val appIcon: android.graphics.drawable.Icon?,
+    val launcherIcon: Icon,
     val useGrayColorFilter: Boolean,
     val artistName: CharSequence,
     val titleName: CharSequence,
     val isExplicitVisible: Boolean,
+    val shouldAddGradient: Boolean,
     val colorScheme: ColorScheme,
-    val isTimeVisible: Boolean,
+    val canShowTime: Boolean,
     val playTurbulenceNoise: Boolean,
     val useSemanticActions: Boolean,
     val actionButtons: List<MediaActionViewModel?>,
@@ -38,4 +40,6 @@
     val gutsMenu: GutsViewModel,
     val onClicked: (Expandable) -> Unit,
     val onLongClicked: () -> Unit,
+    val onSeek: () -> Unit,
+    val onBindSeekbar: (SeekBarViewModel) -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java
index 706ac9c..b43a1d2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java
@@ -23,6 +23,7 @@
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.HardwareRenderer;
+import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.RecordingCanvas;
 import android.graphics.Rect;
@@ -38,9 +39,11 @@
 import android.view.Display;
 import android.view.ScrollCaptureResponse;
 import android.view.View;
+import android.view.WindowInsets;
 import android.widget.ImageView;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.core.view.WindowCompat;
 
 import com.android.internal.app.ChooserActivity;
 import com.android.internal.logging.UiEventLogger;
@@ -127,6 +130,10 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        // Enable edge-to-edge explicitly.
+        WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
+
         setContentView(R.layout.long_screenshot);
 
         mPreview = requireViewById(R.id.preview);
@@ -149,6 +156,13 @@
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
                         updateImageDimensions());
 
+        requireViewById(R.id.root).setOnApplyWindowInsetsListener(
+                (view, windowInsets) -> {
+                    Insets insets = windowInsets.getInsets(WindowInsets.Type.systemBars());
+                    view.setPadding(insets.left, insets.top, insets.right, insets.bottom);
+                    return WindowInsets.CONSUMED;
+                });
+
         Intent intent = getIntent();
         mScrollCaptureResponse = intent.getParcelableExtra(EXTRA_CAPTURE_RESPONSE);
         mScreenshotUserHandle = intent.getParcelableExtra(EXTRA_SCREENSHOT_USER_HANDLE,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index 980f665a..6800c61 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
 import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -64,6 +65,7 @@
     private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
     private val footerActionsController: FooterActionsController,
     private val sceneInteractor: SceneInteractor,
+    unfoldTransitionInteractor: UnfoldTransitionInteractor,
 ) {
     val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
         combine(
@@ -106,6 +108,16 @@
 
     val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode
 
+    /**
+     * The unfold transition progress. When fully-unfolded, this is `1` and fully folded, it's `0`.
+     */
+    val unfoldTransitionProgress: StateFlow<Float> =
+        unfoldTransitionInteractor.unfoldProgress.stateIn(
+            scope = applicationScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = 1f
+        )
+
     /** Notifies that some content in the shade was clicked. */
     fun onContentClicked() {
         if (!isClickable.value) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index a0c5618..5f08afd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -128,6 +128,8 @@
                                 mobileGroupView,
                                 dotView,
                             )
+
+                            view.requestLayout()
                         }
                     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
index 08ed030..054116d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -22,6 +22,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -41,6 +43,7 @@
     val repo: DeviceBasedSatelliteRepository,
     iconsInteractor: MobileIconsInteractor,
     deviceProvisioningInteractor: DeviceProvisioningInteractor,
+    wifiInteractor: WifiInteractor,
     @Application scope: CoroutineScope,
 ) {
     /** Must be observed by any UI showing Satellite iconography */
@@ -73,6 +76,9 @@
 
     val isDeviceProvisioned: Flow<Boolean> = deviceProvisioningInteractor.isDeviceProvisioned
 
+    val isWifiActive: Flow<Boolean> =
+        wifiInteractor.wifiNetwork.map { it is WifiNetworkModel.Active }
+
     /** When all connections are considered OOS, satellite connectivity is potentially valid */
     val areAllConnectionsOutOfService =
         if (Flags.oemEnabledSatelliteFlag()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 40641be..a0291b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -59,9 +59,10 @@
                 combine(
                     interactor.isSatelliteAllowed,
                     interactor.isDeviceProvisioned,
+                    interactor.isWifiActive,
                     airplaneModeRepository.isAirplaneMode
-                ) { isSatelliteAllowed, isDeviceProvisioned, isAirplaneMode ->
-                    isSatelliteAllowed && isDeviceProvisioned && !isAirplaneMode
+                ) { isSatelliteAllowed, isDeviceProvisioned, isWifiActive, isAirplaneMode ->
+                    isSatelliteAllowed && isDeviceProvisioned && !isWifiActive && !isAirplaneMode
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
index 0d3682c..fbbd2b9 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
@@ -15,9 +15,11 @@
  */
 package com.android.systemui.unfold.data.repository
 
+import androidx.annotation.FloatRange
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress
 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
 import com.android.systemui.util.kotlin.getOrNull
 import java.util.Optional
@@ -42,6 +44,10 @@
 sealed class UnfoldTransitionStatus {
     /** Status that is sent when fold or unfold transition is in started state */
     data object TransitionStarted : UnfoldTransitionStatus()
+    /** Status that is sent while fold or unfold transition is in progress */
+    data class TransitionInProgress(
+        @FloatRange(from = 0.0, to = 1.0) val progress: Float,
+    ) : UnfoldTransitionStatus()
     /** Status that is sent when fold or unfold transition is finished */
     data object TransitionFinished : UnfoldTransitionStatus()
 }
@@ -66,6 +72,10 @@
                             trySend(TransitionStarted)
                         }
 
+                        override fun onTransitionProgress(progress: Float) {
+                            trySend(TransitionInProgress(progress))
+                        }
+
                         override fun onTransitionFinished() {
                             trySend(TransitionFinished)
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
index 3e2e564..a8e4496 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
@@ -17,10 +17,14 @@
 
 import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository
 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress
 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
 import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
 
 /**
  * Contains business-logic related to fold-unfold transitions while interacting with
@@ -30,6 +34,8 @@
     /** Returns availability of fold/unfold transitions on the device */
     val isAvailable: Boolean
 
+    val unfoldProgress: Flow<Float>
+
     /** Suspends and waits for a fold/unfold transition to finish */
     suspend fun waitForTransitionFinish()
 
@@ -44,6 +50,11 @@
     override val isAvailable: Boolean
         get() = repository.isAvailable
 
+    override val unfoldProgress: Flow<Float> =
+        repository.transitionStatus
+            .map { (it as? TransitionInProgress)?.progress ?: 1f }
+            .distinctUntilChanged()
+
     override suspend fun waitForTransitionFinish() {
         repository.transitionStatus.filter { it is TransitionFinished }.first()
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
index a249961..319b615 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
@@ -205,9 +205,9 @@
         when(mClockRegistry.createCurrentClock()).thenReturn(mClockController);
         when(mClockEventController.getClock()).thenReturn(mClockController);
         when(mSmallClockController.getConfig())
-                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false));
+                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false, false));
         when(mLargeClockController.getConfig())
-                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false));
+                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false, false));
 
         mSliceView = new View(getContext());
         when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 9d81b96..99b5a4b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -272,9 +272,9 @@
         assertEquals(View.VISIBLE, mFakeDateView.getVisibility());
 
         when(mSmallClockController.getConfig())
-                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false));
+                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false, true));
         when(mLargeClockController.getConfig())
-                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false));
+                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false, true));
         verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture());
         listenerArgumentCaptor.getValue().onCurrentClockChanged();
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index 11fe862..b2828a4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -98,7 +98,7 @@
     public void updatePosition_primaryClockAnimation() {
         ClockController mockClock = mock(ClockController.class);
         when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock);
-        when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", "", "", false, true));
+        when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", "", "", false, true, false));
 
         mController.updatePosition(10, 15, 20f, true);
 
@@ -113,7 +113,7 @@
     public void updatePosition_alternateClockAnimation() {
         ClockController mockClock = mock(ClockController.class);
         when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock);
-        when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", "", "", true, true));
+        when(mockClock.getConfig()).thenReturn(new ClockConfig("MOCK", "", "", true, true, false));
 
         mController.updatePosition(10, 15, 20f, true);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
index 3f05bfa..9ccf212 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -227,4 +228,50 @@
                 { it == KeyguardSurfaceBehindModel(alpha = 0f) },
             )
         }
+
+    @Test
+    fun notificationLaunchFromLockscreen_isAnimatingSurfaceTrue() =
+        testScope.runTest {
+            val isAnimatingSurface by collectLastValue(underTest.isAnimatingSurface)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.LOCKSCREEN,
+                    transitionState = TransitionState.STARTED,
+                )
+            )
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.LOCKSCREEN,
+                    transitionState = TransitionState.FINISHED,
+                )
+            )
+            kosmos.notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(true)
+            runCurrent()
+            assertThat(isAnimatingSurface).isTrue()
+        }
+
+    @Test
+    fun notificationLaunchFromGone_isAnimatingSurfaceFalse() =
+        testScope.runTest {
+            val isAnimatingSurface by collectLastValue(underTest.isAnimatingSurface)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    transitionState = TransitionState.STARTED,
+                )
+            )
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    transitionState = TransitionState.FINISHED,
+                )
+            )
+            kosmos.notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(true)
+            runCurrent()
+            assertThat(isAnimatingSurface).isFalse()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
index 66aa572..5e3a142 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
@@ -113,7 +113,8 @@
                 id = "WEATHER_CLOCK",
                 name = "",
                 description = "",
-                useAlternateSmartspaceAODTransition = true
+                useAlternateSmartspaceAODTransition = true,
+                useCustomClockScene = true
             )
         whenever(clock.config).thenReturn(clockConfig)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
index 4d20f55..8f73811 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -50,6 +50,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Mock
@@ -76,9 +77,10 @@
 @TestableLooper.RunWithLooper
 class MediaDataFilterImplTest : SysuiTestCase() {
 
+    @Mock private lateinit var listener: MediaDataProcessor.Listener
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var broadcastSender: BroadcastSender
-    @Mock private lateinit var mediaDataManager: MediaDataManager
+    @Mock private lateinit var mediaDataProcessor: MediaDataProcessor
     @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
     @Mock private lateinit var executor: Executor
     @Mock private lateinit var smartspaceData: SmartspaceMediaData
@@ -114,7 +116,8 @@
                 mediaFlags,
                 repository,
             )
-        mediaDataFilter.mediaDataManager = mediaDataManager
+        mediaDataFilter.mediaDataProcessor = mediaDataProcessor
+        mediaDataFilter.addListener(listener)
 
         // Start all tests as main user
         setUser(USER_MAIN)
@@ -167,6 +170,8 @@
 
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
             assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel)
         }
 
@@ -178,6 +183,8 @@
 
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
 
+            verify(listener, never())
+                .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
             assertThat(mediaDataLoadedStates).isNotEqualTo(mediaLoadedStatesModel)
         }
 
@@ -196,6 +203,7 @@
             mediaLoadedStatesModel.remove(MediaDataLoadingModel.Loaded(dataMain.instanceId))
             mediaDataFilter.onMediaDataRemoved(KEY)
 
+            verify(listener).onMediaDataRemoved(eq(KEY))
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
         }
 
@@ -208,6 +216,7 @@
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
             mediaDataFilter.onMediaDataRemoved(KEY)
 
+            verify(listener, never()).onMediaDataRemoved(eq(KEY))
             assertThat(mediaDataLoadedStates).isEmpty()
         }
 
@@ -226,6 +235,7 @@
             setUser(USER_GUEST)
 
             // THEN we should remove the main user's media
+            verify(listener).onMediaDataRemoved(eq(KEY))
             assertThat(mediaDataLoadedStates).isEmpty()
         }
 
@@ -243,6 +253,20 @@
             // and we switch to guest user
             setUser(USER_GUEST)
 
+            // THEN we should add back the guest user media
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false))
+
+            // but not the main user's
+            verify(listener, never())
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    any(),
+                    eq(dataMain),
+                    anyBoolean(),
+                    anyInt(),
+                    anyBoolean()
+                )
             assertThat(mediaDataLoadedStates).isEqualTo(guestLoadedStatesModel)
             assertThat(mediaDataLoadedStates).isNotEqualTo(mainLoadedStatesModel)
         }
@@ -261,6 +285,7 @@
 
             val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
             // THEN we should remove the private profile media
+            verify(listener).onMediaDataRemoved(eq(KEY_ALT))
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
         }
 
@@ -481,7 +506,7 @@
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
         mediaDataFilter.onSwipeToDismiss()
 
-        verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true))
+        verify(mediaDataProcessor).setInactive(eq(KEY), eq(true), eq(true))
     }
 
     @Test
@@ -507,6 +532,8 @@
                 )
                 .isTrue()
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -534,6 +561,9 @@
                 )
                 .isFalse()
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(listener, never())
+                .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+            verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -563,6 +593,8 @@
                 )
                 .isTrue()
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -592,6 +624,7 @@
                 )
                 .isFalse()
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -614,6 +647,8 @@
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
             // AND we get a smartspace signal
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -629,6 +664,9 @@
                 )
                 .isFalse()
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(listener, never())
+                .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
+            verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -649,12 +687,15 @@
             val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
             // AND we get a smartspace signal
             runCurrent()
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // THEN we should treat the media as active instead
+            val dataCurrentAndActive = dataCurrent.copy(active = true)
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -664,8 +705,18 @@
                     )
                 )
                 .isTrue()
+            verify(listener)
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    eq(KEY),
+                    eq(dataCurrentAndActive),
+                    eq(true),
+                    eq(100),
+                    eq(true)
+                )
             // Smartspace update shouldn't be propagated for the empty rec list.
             assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown)
+            verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
         }
@@ -687,12 +738,24 @@
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
             assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
             // AND we get a smartspace signal
             runCurrent()
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // THEN we should treat the media as active instead
+            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            verify(listener)
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    eq(KEY),
+                    eq(dataCurrentAndActive),
+                    eq(true),
+                    eq(100),
+                    eq(true)
+                )
             assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -704,6 +767,8 @@
                 .isTrue()
             // Smartspace update should also be propagated but not prioritized.
             assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
         }
@@ -721,6 +786,7 @@
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
             mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
+            verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
             assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -746,15 +812,29 @@
             val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
             runCurrent()
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
+            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            verify(listener)
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    eq(KEY),
+                    eq(dataCurrentAndActive),
+                    eq(true),
+                    eq(100),
+                    eq(true)
+                )
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
 
             mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
+            verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
             assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -781,6 +861,8 @@
 
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
             assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -813,12 +895,18 @@
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
 
             // And an inactive recommendation is loaded
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // Smartspace is loaded but the media stays inactive
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(listener, never())
+                .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
             assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -846,8 +934,8 @@
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data)
         mediaDataFilter.onSwipeToDismiss()
 
-        verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY))
-        verify(mediaDataManager, never())
+        verify(mediaDataProcessor).setRecommendationInactive(eq(SMARTSPACE_KEY))
+        verify(mediaDataProcessor, never())
             .dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong())
     }
 
@@ -866,6 +954,8 @@
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
 
             // AND we get a smartspace signal with extra to trigger resume
@@ -875,6 +965,16 @@
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // THEN we should treat the media as active instead
+            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            verify(listener)
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    eq(KEY),
+                    eq(dataCurrentAndActive),
+                    eq(true),
+                    eq(100),
+                    eq(true)
+                )
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -886,6 +986,8 @@
                 .isTrue()
             // And update the smartspace data state, but not prioritized
             assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
         }
 
     @Test
@@ -901,6 +1003,8 @@
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
             assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
 
             // AND we get a smartspace signal with extra to not trigger resume
@@ -908,7 +1012,12 @@
             whenever(cardAction.extras).thenReturn(extras)
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
+            // THEN listeners are not updated to show media
+            verify(listener, never())
+                .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true))
             // But the smartspace update is still propagated
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
             assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
index a73bb2c..e5d3082 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
@@ -16,29 +16,54 @@
 
 package com.android.systemui.media.controls.ui.controller
 
+import android.animation.AnimatorSet
+import android.content.Context
 import android.content.res.Configuration
 import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.RippleDrawable
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
+import android.view.ViewGroup
+import android.view.animation.Interpolator
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.SeekBar
+import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.lifecycle.LiveData
 import androidx.test.filters.SmallTest
+import com.android.internal.widget.CachingIconView
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.ui.view.GutsViewHolder
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.controls.ui.view.MediaViewHolder
 import com.android.systemui.media.controls.ui.view.RecommendationViewHolder
+import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.res.R
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView
+import com.android.systemui.surfaceeffects.ripple.MultiRippleView
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
 import com.android.systemui.util.animation.MeasurementInput
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionViewState
 import com.android.systemui.util.animation.WidgetState
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.floatThat
 import org.mockito.Mock
+import org.mockito.Mockito
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
@@ -55,6 +80,31 @@
         com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
     private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
     private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+    private val clock = FakeSystemClock()
+    private lateinit var mainExecutor: FakeExecutor
+    private lateinit var seekBar: SeekBar
+    private lateinit var multiRippleView: MultiRippleView
+    private lateinit var turbulenceNoiseView: TurbulenceNoiseView
+    private lateinit var loadingEffectView: LoadingEffectView
+    private lateinit var settings: ImageButton
+    private lateinit var cancel: View
+    private lateinit var cancelText: TextView
+    private lateinit var dismiss: FrameLayout
+    private lateinit var dismissText: TextView
+    private lateinit var titleText: TextView
+    private lateinit var artistText: TextView
+    private lateinit var explicitIndicator: CachingIconView
+    private lateinit var seamless: ViewGroup
+    private lateinit var seamlessButton: View
+    private lateinit var seamlessIcon: ImageView
+    private lateinit var seamlessText: TextView
+    private lateinit var scrubbingElapsedTimeView: TextView
+    private lateinit var scrubbingTotalTimeView: TextView
+    private lateinit var actionPlayPause: ImageButton
+    private lateinit var actionNext: ImageButton
+    private lateinit var actionPrev: ImageButton
+    @Mock private lateinit var seamlessBackground: RippleDrawable
+    @Mock private lateinit var albumView: ImageView
     @Mock lateinit var logger: MediaViewLogger
     @Mock private lateinit var mockViewState: TransitionViewState
     @Mock private lateinit var mockCopiedState: TransitionViewState
@@ -64,6 +114,14 @@
     @Mock private lateinit var mediaSubTitleWidgetState: WidgetState
     @Mock private lateinit var mediaContainerWidgetState: WidgetState
     @Mock private lateinit var mediaFlags: MediaFlags
+    @Mock private lateinit var seekBarViewModel: SeekBarViewModel
+    @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
+    @Mock private lateinit var globalSettings: GlobalSettings
+    @Mock private lateinit var viewHolder: MediaViewHolder
+    @Mock private lateinit var view: TransitionLayout
+    @Mock private lateinit var mockAnimator: AnimatorSet
+    @Mock private lateinit var gutsViewHolder: GutsViewHolder
+    @Mock private lateinit var gutsText: TextView
 
     private val delta = 0.1F
 
@@ -72,14 +130,30 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        mainExecutor = FakeExecutor(clock)
         mediaViewController =
-            MediaViewController(
-                context,
-                configurationController,
-                mediaHostStatesManager,
-                logger,
-                mediaFlags,
-            )
+            object :
+                MediaViewController(
+                    context,
+                    configurationController,
+                    mediaHostStatesManager,
+                    logger,
+                    seekBarViewModel,
+                    mainExecutor,
+                    mediaFlags,
+                    globalSettings,
+                ) {
+                override fun loadAnimator(
+                    context: Context,
+                    animId: Int,
+                    motionInterpolator: Interpolator?,
+                    vararg targets: View?
+                ): AnimatorSet {
+                    return mockAnimator
+                }
+            }
+        initGutsViewHolderMocks()
+        initMediaViewHolderMocks()
     }
 
     @Test
@@ -299,4 +373,270 @@
         verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
         verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
     }
+
+    @Test
+    fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar))
+            .isEqualTo(ConstraintSet.INVISIBLE)
+    }
+
+    @Test
+    fun attachPlayer_seekBarEnabled_seekBarVisible() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar))
+            .isEqualTo(ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun attachPlayer_seekBarStatusUpdate_seekBarVisibilityChanges() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar))
+            .isEqualTo(ConstraintSet.VISIBLE)
+
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.media_progress_bar))
+            .isEqualTo(ConstraintSet.INVISIBLE)
+    }
+
+    @Test
+    fun attachPlayer_notScrubbing_scrubbingViewsGone() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        mediaViewController.canShowScrubbingTime = true
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        getScrubbingChangeListener().onScrubbingChanged(false)
+        mainExecutor.runAllReady()
+
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_noSemanticActions_scrubbingViewsGone() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        mediaViewController.canShowScrubbingTime = false
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        mediaViewController.setUpNextButtonInfo(true)
+        mediaViewController.setUpPrevButtonInfo(false)
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext))
+            .isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        mediaViewController.setUpNextButtonInfo(false)
+        mediaViewController.setUpPrevButtonInfo(true)
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev))
+            .isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        mediaViewController.setUpNextButtonInfo(true)
+        mediaViewController.setUpPrevButtonInfo(true)
+        mediaViewController.canShowScrubbingTime = true
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        // Only in expanded, we should show the scrubbing times and hide prev+next
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev))
+            .isEqualTo(ConstraintSet.GONE)
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext))
+            .isEqualTo(ConstraintSet.GONE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time)
+            )
+            .isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time)
+            )
+            .isEqualTo(ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun setIsScrubbing_trueThenFalse_reservePrevAndNextButtons() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
+        mediaViewController.attachPlayer(viewHolder)
+        mediaViewController.setUpNextButtonInfo(true, ConstraintSet.INVISIBLE)
+        mediaViewController.setUpPrevButtonInfo(true, ConstraintSet.INVISIBLE)
+        mediaViewController.canShowScrubbingTime = true
+
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev))
+            .isEqualTo(ConstraintSet.INVISIBLE)
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext))
+            .isEqualTo(ConstraintSet.INVISIBLE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time)
+            )
+            .isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time)
+            )
+            .isEqualTo(ConstraintSet.VISIBLE)
+
+        getScrubbingChangeListener().onScrubbingChanged(false)
+        mainExecutor.runAllReady()
+
+        // Only in expanded, we should hide the scrubbing times and show prev+next
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionPrev))
+            .isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(mediaViewController.expandedLayout.getVisibility(R.id.actionNext))
+            .isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_elapsed_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+        assertThat(
+                mediaViewController.expandedLayout.getVisibility(R.id.media_scrubbing_total_time)
+            )
+            .isEqualTo(ConstraintSet.GONE)
+    }
+
+    private fun initGutsViewHolderMocks() {
+        settings = ImageButton(context)
+        cancel = View(context)
+        cancelText = TextView(context)
+        dismiss = FrameLayout(context)
+        dismissText = TextView(context)
+        whenever(gutsViewHolder.gutsText).thenReturn(gutsText)
+        whenever(gutsViewHolder.settings).thenReturn(settings)
+        whenever(gutsViewHolder.cancel).thenReturn(cancel)
+        whenever(gutsViewHolder.cancelText).thenReturn(cancelText)
+        whenever(gutsViewHolder.dismiss).thenReturn(dismiss)
+        whenever(gutsViewHolder.dismissText).thenReturn(dismissText)
+    }
+
+    private fun initMediaViewHolderMocks() {
+        titleText = TextView(context)
+        artistText = TextView(context)
+        explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator }
+        seamless = FrameLayout(context)
+        seamless.foreground = seamlessBackground
+        seamlessButton = View(context)
+        seamlessIcon = ImageView(context)
+        seamlessText = TextView(context)
+        seekBar = SeekBar(context).also { it.id = R.id.media_progress_bar }
+
+        actionPlayPause = ImageButton(context).also { it.id = R.id.actionPlayPause }
+        actionPrev = ImageButton(context).also { it.id = R.id.actionPrev }
+        actionNext = ImageButton(context).also { it.id = R.id.actionNext }
+        scrubbingElapsedTimeView =
+            TextView(context).also { it.id = R.id.media_scrubbing_elapsed_time }
+        scrubbingTotalTimeView = TextView(context).also { it.id = R.id.media_scrubbing_total_time }
+
+        multiRippleView = MultiRippleView(context, null)
+        turbulenceNoiseView = TurbulenceNoiseView(context, null)
+        loadingEffectView = LoadingEffectView(context, null)
+
+        whenever(viewHolder.player).thenReturn(view)
+        whenever(view.context).thenReturn(context)
+        whenever(viewHolder.albumView).thenReturn(albumView)
+        whenever(albumView.foreground).thenReturn(Mockito.mock(Drawable::class.java))
+        whenever(viewHolder.titleText).thenReturn(titleText)
+        whenever(viewHolder.artistText).thenReturn(artistText)
+        whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator)
+        whenever(seamlessBackground.getDrawable(0))
+            .thenReturn(Mockito.mock(GradientDrawable::class.java))
+        whenever(viewHolder.seamless).thenReturn(seamless)
+        whenever(viewHolder.seamlessButton).thenReturn(seamlessButton)
+        whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon)
+        whenever(viewHolder.seamlessText).thenReturn(seamlessText)
+        whenever(viewHolder.seekBar).thenReturn(seekBar)
+        whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
+        whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
+        whenever(viewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
+        whenever(seekBarViewModel.progress).thenReturn(seekBarData)
+
+        // Action buttons
+        whenever(viewHolder.actionPlayPause).thenReturn(actionPlayPause)
+        whenever(viewHolder.getAction(R.id.actionNext)).thenReturn(actionNext)
+        whenever(viewHolder.getAction(R.id.actionPrev)).thenReturn(actionPrev)
+        whenever(viewHolder.getAction(R.id.actionPlayPause)).thenReturn(actionPlayPause)
+
+        whenever(viewHolder.multiRippleView).thenReturn(multiRippleView)
+        whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView)
+        whenever(viewHolder.loadingEffectView).thenReturn(loadingEffectView)
+    }
+
+    private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
+        withArgCaptor {
+            verify(seekBarViewModel).setScrubbingChangeListener(capture())
+        }
+
+    private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor {
+        verify(seekBarViewModel).setEnabledChangeListener(capture())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
index 56fc0c7..a05a23b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction
 import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
@@ -34,21 +34,19 @@
 @RunWith(AndroidTestingRunner::class)
 class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() {
 
-    private val progressProvider = TestUnfoldTransitionProvider()
+    private val progressProvider = FakeUnfoldTransitionProvider()
 
-    @Mock
-    private lateinit var parent: ViewGroup
+    @Mock private lateinit var parent: ViewGroup
 
-    @Mock
-    private lateinit var shouldBeAnimated: () -> Boolean
+    @Mock private lateinit var shouldBeAnimated: () -> Boolean
 
     private lateinit var animator: UnfoldConstantTranslateAnimator
 
     private val viewsIdToRegister
         get() =
             setOf(
-                    ViewIdToTranslate(START_VIEW_ID, Direction.START, shouldBeAnimated),
-                    ViewIdToTranslate(END_VIEW_ID, Direction.END, shouldBeAnimated)
+                ViewIdToTranslate(START_VIEW_ID, Direction.START, shouldBeAnimated),
+                ViewIdToTranslate(END_VIEW_ID, Direction.END, shouldBeAnimated)
             )
 
     @Before
@@ -122,11 +120,12 @@
         progressProvider.onTransitionStarted()
         progressProvider.onTransitionProgress(0f)
 
-        val rtlMultiplier = if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
-            1
-        } else {
-            -1
-        }
+        val rtlMultiplier =
+            if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+                1
+            } else {
+                -1
+            }
         list.forEach { (view, direction) ->
             assertEquals(
                 (-MAX_TRANSLATION * direction * rtlMultiplier).toInt(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
index 4bfd7e3..df82df8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
 import com.android.systemui.power.shared.model.WakefulnessState.STARTING_TO_SLEEP
 import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl
 import com.android.systemui.util.animation.data.repository.FakeAnimationStatusRepository
@@ -59,7 +59,7 @@
 
     private val animationStatus = FakeAnimationStatusRepository()
     private val configurationController = FakeConfigurationController()
-    private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider()
+    private val unfoldTransitionProgressProvider = FakeUnfoldTransitionProvider()
     private val powerRepository = FakePowerRepository()
     private val powerInteractor =
         PowerInteractor(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt
index feff046..1ec1765 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt
@@ -8,7 +8,7 @@
 import android.view.WindowManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import com.android.systemui.unfold.util.CurrentActivityTypeProvider
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
 import com.android.systemui.util.mockito.whenever
@@ -23,17 +23,14 @@
 @TestableLooper.RunWithLooper
 class StatusBarMoveFromCenterAnimationControllerTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var windowManager: WindowManager
+    @Mock private lateinit var windowManager: WindowManager
 
-    @Mock
-    private lateinit var display: Display
+    @Mock private lateinit var display: Display
 
-    @Mock
-    private lateinit var currentActivityTypeProvider: CurrentActivityTypeProvider
+    @Mock private lateinit var currentActivityTypeProvider: CurrentActivityTypeProvider
 
     private val view: View = View(context)
-    private val progressProvider = TestUnfoldTransitionProvider()
+    private val progressProvider = FakeUnfoldTransitionProvider()
     private val scopedProvider = ScopedUnfoldTransitionProgressProvider(progressProvider)
 
     private lateinit var controller: StatusBarMoveFromCenterAnimationController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
index 42bbe3e..c4ab943 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -26,6 +26,10 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
 import com.android.systemui.util.mockito.mock
@@ -53,6 +57,10 @@
     private val deviceProvisionedRepository = FakeDeviceProvisioningRepository()
     private val deviceProvisioningInteractor =
         DeviceProvisioningInteractor(deviceProvisionedRepository)
+    private val connectivityRepository = FakeConnectivityRepository()
+    private val wifiRepository = FakeWifiRepository()
+    private val wifiInteractor =
+        WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope)
 
     @Before
     fun setUp() {
@@ -61,6 +69,7 @@
                 repo,
                 iconsInteractor,
                 deviceProvisioningInteractor,
+                wifiInteractor,
                 testScope.backgroundScope,
             )
     }
@@ -103,6 +112,7 @@
                     repo,
                     iconsInteractor,
                     deviceProvisioningInteractor,
+                    wifiInteractor,
                     testScope.backgroundScope,
                 )
 
@@ -150,6 +160,7 @@
                     repo,
                     iconsInteractor,
                     deviceProvisioningInteractor,
+                    wifiInteractor,
                     testScope.backgroundScope,
                 )
 
@@ -205,6 +216,7 @@
                     repo,
                     iconsInteractor,
                     deviceProvisioningInteractor,
+                    wifiInteractor,
                     testScope.backgroundScope,
                 )
 
@@ -337,6 +349,7 @@
                     repo,
                     iconsInteractor,
                     deviceProvisioningInteractor,
+                    wifiInteractor,
                     testScope.backgroundScope,
                 )
 
@@ -353,4 +366,28 @@
             // THEN the value is still false, because the flag is off
             assertThat(latest).isFalse()
         }
+
+    @Test
+    fun isWifiActive_falseWhenWifiNotActive() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiActive)
+
+            // WHEN wifi is not active
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Invalid("test"))
+
+            // THEN the interactor returns false due to the wifi network not being active
+            assertThat(latest).isFalse()
+        }
+
+    @Test
+    fun isWifiActive_trueWhenWifiIsActive() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isWifiActive)
+
+            // WHEN wifi is active
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 0, level = 1))
+
+            // THEN the interactor returns true due to the wifi network being active
+            assertThat(latest).isTrue()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 1d6cd37..64f19b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -26,6 +26,10 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
 import com.android.systemui.util.mockito.mock
@@ -44,14 +48,18 @@
     private lateinit var underTest: DeviceBasedSatelliteViewModel
     private lateinit var interactor: DeviceBasedSatelliteInteractor
     private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
-
     private val repo = FakeDeviceBasedSatelliteRepository()
+    private val testScope = TestScope()
+
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
     private val deviceProvisionedRepository = FakeDeviceProvisioningRepository()
     private val deviceProvisioningInteractor =
         DeviceProvisioningInteractor(deviceProvisionedRepository)
-
-    private val testScope = TestScope()
+    private val connectivityRepository = FakeConnectivityRepository()
+    private val wifiRepository = FakeWifiRepository()
+    private val wifiInteractor =
+        WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope)
 
     @Before
     fun setUp() {
@@ -63,6 +71,7 @@
                 repo,
                 mobileIconsInteractor,
                 deviceProvisioningInteractor,
+                wifiInteractor,
                 testScope.backgroundScope,
             )
 
@@ -253,4 +262,40 @@
             // THEN icon is null because the device is not provisioned
             assertThat(latest).isInstanceOf(Icon::class.java)
         }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun icon_wifiIsActive() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+            i1.isEmergencyOnly.value = false
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // GIVEN device is provisioned
+            deviceProvisionedRepository.setDeviceProvisioned(true)
+
+            // GIVEN wifi network is active
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 0, level = 1))
+
+            // THEN icon is null because the device is connected to wifi
+            assertThat(latest).isNull()
+
+            // GIVEN device loses wifi connection
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Invalid("test"))
+
+            // Wait for delay to be completed
+            advanceTimeBy(10.seconds)
+
+            // THEN icon is set because the device lost wifi connection
+            assertThat(latest).isInstanceOf(Icon::class.java)
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
index 28adbce..383f4a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
@@ -86,7 +86,7 @@
     private val areAnimationEnabled = MutableStateFlow(true)
     private val lastWakefulnessEvent = MutableStateFlow(WakefulnessModel())
     private val systemClock = FakeSystemClock()
-    private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider()
+    private val unfoldTransitionProgressProvider = FakeUnfoldTransitionProvider()
     private val unfoldTransitionRepository =
         UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider))
     private val unfoldTransitionInteractor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
index b9c7e61..fd513c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
@@ -35,7 +35,7 @@
 @SmallTest
 class UnfoldHapticsPlayerTest : SysuiTestCase() {
 
-    private val progressProvider = TestUnfoldTransitionProvider()
+    private val progressProvider = FakeUnfoldTransitionProvider()
     private val vibrator: Vibrator = mock()
     private val testFoldProvider = TestFoldProvider()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
index ba72716..2955384 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.unfold.util.FoldableDeviceStates
 import com.android.systemui.unfold.util.FoldableTestUtils
 import com.android.systemui.util.mockito.any
+import java.util.Optional
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -37,45 +38,41 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations
-import java.util.Optional
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class UnfoldLatencyTrackerTest : SysuiTestCase() {
 
-    @Mock
-    lateinit var latencyTracker: LatencyTracker
+    @Mock lateinit var latencyTracker: LatencyTracker
 
-    @Mock
-    lateinit var deviceStateManager: DeviceStateManager
+    @Mock lateinit var deviceStateManager: DeviceStateManager
 
-    @Mock
-    lateinit var screenLifecycle: ScreenLifecycle
+    @Mock lateinit var screenLifecycle: ScreenLifecycle
 
-    @Captor
-    private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener>
+    @Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener>
 
-    @Captor
-    private lateinit var screenLifecycleCaptor: ArgumentCaptor<ScreenLifecycle.Observer>
+    @Captor private lateinit var screenLifecycleCaptor: ArgumentCaptor<ScreenLifecycle.Observer>
 
     private lateinit var deviceStates: FoldableDeviceStates
 
     private lateinit var unfoldLatencyTracker: UnfoldLatencyTracker
 
-    private val transitionProgressProvider = TestUnfoldTransitionProvider()
+    private val transitionProgressProvider = FakeUnfoldTransitionProvider()
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        unfoldLatencyTracker = UnfoldLatencyTracker(
-            latencyTracker,
-            deviceStateManager,
-            Optional.of(transitionProgressProvider),
-            context.mainExecutor,
-            context,
-            context.contentResolver,
-            screenLifecycle
-        ).apply { init() }
+        unfoldLatencyTracker =
+            UnfoldLatencyTracker(
+                    latencyTracker,
+                    deviceStateManager,
+                    Optional.of(transitionProgressProvider),
+                    context.mainExecutor,
+                    context,
+                    context.contentResolver,
+                    screenLifecycle
+                )
+                .apply { init() }
         deviceStates = FoldableTestUtils.findDeviceStates(context)
 
         verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture())
@@ -107,7 +104,7 @@
     }
 
     @Test
-    fun unfold_firstFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventNotPropagated() {
+    fun firstFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventNotPropagated() {
         setAnimationsEnabled(true)
         sendFoldEvent(folded = false)
 
@@ -118,7 +115,7 @@
     }
 
     @Test
-    fun unfold_secondFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() {
+    fun secondFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() {
         setAnimationsEnabled(true)
         sendFoldEvent(folded = true)
         sendFoldEvent(folded = false)
@@ -131,7 +128,7 @@
     }
 
     @Test
-    fun unfold_unfoldFoldUnfoldAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() {
+    fun unfoldFoldUnfoldAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() {
         setAnimationsEnabled(true)
         sendFoldEvent(folded = false)
         sendFoldEvent(folded = true)
@@ -196,4 +193,4 @@
             durationScale.toString()
         )
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt
index 6ec0251..0c452eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt
@@ -17,21 +17,18 @@
 @SmallTest
 class UnfoldTransitionWallpaperControllerTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var wallpaperController: WallpaperController
+    @Mock private lateinit var wallpaperController: WallpaperController
 
-    private val progressProvider = TestUnfoldTransitionProvider()
+    private val progressProvider = FakeUnfoldTransitionProvider()
 
-    @JvmField
-    @Rule
-    val mockitoRule = MockitoJUnit.rule()
+    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
 
     private lateinit var unfoldWallpaperController: UnfoldTransitionWallpaperController
 
     @Before
     fun setup() {
-        unfoldWallpaperController = UnfoldTransitionWallpaperController(progressProvider,
-            wallpaperController)
+        unfoldWallpaperController =
+            UnfoldTransitionWallpaperController(progressProvider, wallpaperController)
         unfoldWallpaperController.init()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt
index 2bc05fc..e5f619b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt
@@ -23,7 +23,7 @@
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import com.android.systemui.utils.os.FakeHandler
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
@@ -34,7 +34,7 @@
 @RunWithLooper(setAsMainLooper = true)
 class MainThreadUnfoldTransitionProgressProviderTest : SysuiTestCase() {
 
-    private val wrappedProgressProvider = TestUnfoldTransitionProvider()
+    private val wrappedProgressProvider = FakeUnfoldTransitionProvider()
     private val fakeHandler = FakeHandler(Looper.getMainLooper())
     private val listener = TestUnfoldProgressListener()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
index d864d53..70ec050 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
@@ -20,7 +20,7 @@
 import android.view.Surface
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
 import com.android.systemui.unfold.updates.RotationChangeProvider
 import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
@@ -43,14 +43,14 @@
 
     @Mock lateinit var rotationChangeProvider: RotationChangeProvider
 
-    private val sourceProvider = TestUnfoldTransitionProvider()
+    private val sourceProvider = FakeUnfoldTransitionProvider()
 
     @Mock lateinit var transitionListener: TransitionProgressListener
 
     @Captor private lateinit var rotationListenerCaptor: ArgumentCaptor<RotationListener>
 
     lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
-    private lateinit var testableLooper : TestableLooper
+    private lateinit var testableLooper: TestableLooper
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt
index 2f29b3b..451bd24 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt
@@ -22,7 +22,7 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
 import com.android.systemui.util.mockito.any
 import org.junit.Before
@@ -42,7 +42,7 @@
 
     @Mock lateinit var sinkProvider: TransitionProgressListener
 
-    private val sourceProvider = TestUnfoldTransitionProvider()
+    private val sourceProvider = FakeUnfoldTransitionProvider()
 
     private lateinit var contentResolver: ContentResolver
     private lateinit var progressProvider: ScaleAwareTransitionProgressProvider
@@ -132,6 +132,6 @@
             durationScale.toString()
         )
 
-        animatorDurationScaleListenerCaptor.value.dispatchChange(/* selfChange= */false)
+        animatorDurationScaleListenerCaptor.value.dispatchChange(/* selfChange= */ false)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt
index 95c934e..4486402 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt
@@ -23,7 +23,7 @@
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import com.android.systemui.unfold.progress.TestUnfoldProgressListener
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.seconds
@@ -43,7 +43,7 @@
 @RunWithLooper
 class ScopedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
 
-    private val rootProvider = TestUnfoldTransitionProvider()
+    private val rootProvider = FakeUnfoldTransitionProvider()
     private val listener = TestUnfoldProgressListener()
     private val testScope = TestScope(UnconfinedTestDispatcher())
     private val bgThread =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt
index f484ea0..cd4d7b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/UnfoldOnlyProgressProviderTest.kt
@@ -19,7 +19,7 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import com.android.systemui.unfold.progress.TestUnfoldProgressListener
 import com.google.common.util.concurrent.MoreExecutors
 import org.junit.Before
@@ -32,7 +32,7 @@
 class UnfoldOnlyProgressProviderTest : SysuiTestCase() {
 
     private val listener = TestUnfoldProgressListener()
-    private val sourceProvider = TestUnfoldTransitionProvider()
+    private val sourceProvider = FakeUnfoldTransitionProvider()
 
     private val foldProvider = TestFoldProvider()
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt
index da2170c..2f3d3c3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.ui.viewmodel
 
 import android.content.applicationContext
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.media.controls.domain.pipeline.interactor.mediaControlInteractor
@@ -27,6 +28,7 @@
         MediaControlViewModel(
             applicationContext = applicationContext,
             backgroundDispatcher = testDispatcher,
+            backgroundExecutor = fakeExecutor,
             interactor = mediaControlInteractor,
             logger = mediaUiEventLogger,
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/FakeUnfoldTransitionProvider.kt
similarity index 93%
rename from packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/unfold/FakeUnfoldTransitionProvider.kt
index 56c6245..94f0c44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/FakeUnfoldTransitionProvider.kt
@@ -2,7 +2,7 @@
 
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
 
-class TestUnfoldTransitionProvider : UnfoldTransitionProgressProvider, TransitionProgressListener {
+class FakeUnfoldTransitionProvider : UnfoldTransitionProgressProvider, TransitionProgressListener {
 
     private val listeners = mutableListOf<TransitionProgressListener>()
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/UnfoldTransitionProgressProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/UnfoldTransitionProgressProviderKosmos.kt
index 7c54a57..a0f5b58 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/UnfoldTransitionProgressProviderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/UnfoldTransitionProgressProviderKosmos.kt
@@ -18,6 +18,8 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.unfoldTransitionProgressProvider by Fixture { mock<UnfoldTransitionProgressProvider>() }
+val Kosmos.fakeUnfoldTransitionProgressProvider by Fixture { FakeUnfoldTransitionProvider() }
+
+val Kosmos.unfoldTransitionProgressProvider by
+    Fixture<UnfoldTransitionProgressProvider> { fakeUnfoldTransitionProgressProvider }
diff --git a/packages/SystemUI/utils/Android.bp b/packages/SystemUI/utils/Android.bp
new file mode 100644
index 0000000..1ef3816
--- /dev/null
+++ b/packages/SystemUI/utils/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+java_library {
+    name: "SystemUI-shared-utils",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    static_libs: [
+        "kotlin-stdlib",
+        "kotlinx_coroutines",
+    ],
+}
diff --git a/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/FlowConflated.kt b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/FlowConflated.kt
new file mode 100644
index 0000000..ed97c60
--- /dev/null
+++ b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/FlowConflated.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalTypeInference::class)
+
+package com.android.systemui.utils.coroutines.flow
+
+import kotlin.experimental.ExperimentalTypeInference
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.produceIn
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] provided to
+ * the builder's [block] of code via [ProducerScope]. It allows elements to be produced by code that
+ * is running in a different context or concurrently.
+ *
+ * The resulting flow is _cold_, which means that [block] is called every time a terminal operator
+ * is applied to the resulting flow.
+ *
+ * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope]
+ * can be used from any context, e.g. from a callback-based API. The resulting flow completes as
+ * soon as the code in the [block] completes. [awaitClose] should be used to keep the flow running,
+ * otherwise the channel will be closed immediately when block completes. [awaitClose] argument is
+ * called either when a flow consumer cancels the flow collection or when a callback-based API
+ * invokes [SendChannel.close] manually and is typically used to cleanup the resources after the
+ * completion, e.g. unregister a callback. Using [awaitClose] is mandatory in order to prevent
+ * memory leaks when the flow collection is cancelled, otherwise the callback may keep running even
+ * when the flow collector is already completed. To avoid such leaks, this method throws
+ * [IllegalStateException] if block returns, but the channel is not closed yet.
+ *
+ * A [conflated][conflate] channel is used. Use the [buffer] operator on the resulting flow to
+ * specify a user-defined value and to control what happens when data is produced faster than
+ * consumed, i.e. to control the back-pressure behavior.
+ *
+ * Adjacent applications of [callbackFlow], [flowOn], [buffer], and [produceIn] are always fused so
+ * that only one properly configured channel is used for execution.
+ *
+ * Example of usage that converts a multi-shot callback API to a flow. For single-shot callbacks use
+ * [suspendCancellableCoroutine].
+ *
+ * ```
+ * fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow {
+ *     val callback = object : Callback { // Implementation of some callback interface
+ *         override fun onNextValue(value: T) {
+ *             // To avoid blocking you can configure channel capacity using
+ *             // either buffer(Channel.CONFLATED) or buffer(Channel.UNLIMITED) to avoid overfill
+ *             trySendBlocking(value)
+ *                 .onFailure { throwable ->
+ *                     // Downstream has been cancelled or failed, can log here
+ *                 }
+ *         }
+ *         override fun onApiError(cause: Throwable) {
+ *             cancel(CancellationException("API Error", cause))
+ *         }
+ *         override fun onCompleted() = channel.close()
+ *     }
+ *     api.register(callback)
+ *     /*
+ *      * Suspends until either 'onCompleted'/'onApiError' from the callback is invoked
+ *      * or flow collector is cancelled (e.g. by 'take(1)' or because a collector's coroutine was cancelled).
+ *      * In both cases, callback will be properly unregistered.
+ *      */
+ *     awaitClose { api.unregister(callback) }
+ * }
+ * ```
+ * > The callback `register`/`unregister` methods provided by an external API must be thread-safe,
+ * > because `awaitClose` block can be called at any time due to asynchronous nature of
+ * > cancellation, even concurrently with the call of the callback.
+ *
+ * This builder is to be preferred over [callbackFlow], due to the latter's default configuration of
+ * using an internal buffer, negatively impacting system health.
+ *
+ * @see callbackFlow
+ */
+fun <T> conflatedCallbackFlow(
+    @BuilderInference block: suspend ProducerScope<T>.() -> Unit,
+): Flow<T> = callbackFlow(block).conflate()
+
+/**
+ * Creates an instance of a _cold_ [Flow] with elements that are sent to a [SendChannel] provided to
+ * the builder's [block] of code via [ProducerScope]. It allows elements to be produced by code that
+ * is running in a different context or concurrently. The resulting flow is _cold_, which means that
+ * [block] is called every time a terminal operator is applied to the resulting flow.
+ *
+ * This builder ensures thread-safety and context preservation, thus the provided [ProducerScope]
+ * can be used concurrently from different contexts. The resulting flow completes as soon as the
+ * code in the [block] and all its children completes. Use [awaitClose] as the last statement to
+ * keep it running. A more detailed example is provided in the documentation of [callbackFlow].
+ *
+ * A [conflated][conflate] channel is used. Use the [buffer] operator on the resulting flow to
+ * specify a user-defined value and to control what happens when data is produced faster than
+ * consumed, i.e. to control the back-pressure behavior.
+ *
+ * Adjacent applications of [channelFlow], [flowOn], [buffer], and [produceIn] are always fused so
+ * that only one properly configured channel is used for execution.
+ *
+ * Examples of usage:
+ * ```
+ * fun <T> Flow<T>.merge(other: Flow<T>): Flow<T> = channelFlow {
+ *     // collect from one coroutine and send it
+ *     launch {
+ *         collect { send(it) }
+ *     }
+ *     // collect and send from this coroutine, too, concurrently
+ *     other.collect { send(it) }
+ * }
+ *
+ * fun <T> contextualFlow(): Flow<T> = channelFlow {
+ *     // send from one coroutine
+ *     launch(Dispatchers.IO) {
+ *         send(computeIoValue())
+ *     }
+ *     // send from another coroutine, concurrently
+ *     launch(Dispatchers.Default) {
+ *         send(computeCpuValue())
+ *     }
+ * }
+ * ```
+ *
+ * This builder is to be preferred over [channelFlow], due to the latter's default configuration of
+ * using an internal buffer, negatively impacting system health.
+ *
+ * @see channelFlow
+ */
+fun <T> conflatedChannelFlow(
+    @BuilderInference block: suspend ProducerScope<T>.() -> Unit,
+): Flow<T> = channelFlow(block).conflate()
diff --git a/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt
new file mode 100644
index 0000000..5f8c660
--- /dev/null
+++ b/packages/SystemUI/utils/src/com/android/systemui/utils/coroutines/flow/LatestConflated.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalTypeInference::class)
+
+package com.android.systemui.utils.coroutines.flow
+
+import kotlin.experimental.ExperimentalTypeInference
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.transformLatest
+
+/**
+ * Returns a flow that emits elements from the original flow transformed by [transform] function.
+ * When the original flow emits a new value, computation of the [transform] block for previous value
+ * is cancelled.
+ *
+ * For example, the following flow:
+ * ```
+ * flow {
+ *     emit("a")
+ *     delay(100)
+ *     emit("b")
+ * }.mapLatest { value ->
+ *     println("Started computing $value")
+ *     delay(200)
+ *     "Computed $value"
+ * }
+ * ```
+ *
+ * will print "Started computing a" and "Started computing b", but the resulting flow will contain
+ * only "Computed b" value.
+ *
+ * This operator is [conflated][conflate] by default, and as such should be preferred over usage of
+ * [mapLatest], due to the latter's default configuration of using an internal buffer, negatively
+ * impacting system health.
+ *
+ * @see mapLatest
+ */
+fun <T, R> Flow<T>.mapLatestConflated(@BuilderInference transform: suspend (T) -> R): Flow<R> =
+    mapLatest(transform).conflate()
+
+/**
+ * Returns a flow that switches to a new flow produced by [transform] function every time the
+ * original flow emits a value. When the original flow emits a new value, the previous flow produced
+ * by `transform` block is cancelled.
+ *
+ * For example, the following flow:
+ * ```
+ * flow {
+ *     emit("a")
+ *     delay(100)
+ *     emit("b")
+ * }.flatMapLatest { value ->
+ *     flow {
+ *         emit(value)
+ *         delay(200)
+ *         emit(value + "_last")
+ *     }
+ * }
+ * ```
+ *
+ * produces `a b b_last`
+ *
+ * This operator is [conflated][conflate] by default, and as such should be preferred over usage of
+ * [flatMapLatest], due to the latter's default configuration of using an internal buffer,
+ * negatively impacting system health.
+ *
+ * @see flatMapLatest
+ */
+fun <T, R> Flow<T>.flatMapLatestConflated(
+    @BuilderInference transform: suspend (T) -> Flow<R>,
+): Flow<R> = flatMapLatest(transform).conflate()
+
+/**
+ * Returns a flow that produces element by [transform] function every time the original flow emits a
+ * value. When the original flow emits a new value, the previous `transform` block is cancelled,
+ * thus the name `transformLatest`.
+ *
+ * For example, the following flow:
+ * ```
+ * flow {
+ *     emit("a")
+ *     delay(100)
+ *     emit("b")
+ * }.transformLatest { value ->
+ *     emit(value)
+ *     delay(200)
+ *     emit(value + "_last")
+ * }
+ * ```
+ *
+ * produces `a b b_last`.
+ *
+ * This operator is [conflated][conflate] by default, and as such should be preferred over usage of
+ * [transformLatest], due to the latter's default configuration of using an internal buffer,
+ * negatively impacting system health.
+ *
+ * @see transformLatest
+ */
+fun <T, R> Flow<T>.transformLatestConflated(
+    @BuilderInference transform: suspend FlowCollector<R>.(T) -> Unit,
+): Flow<R> = transformLatest(transform).conflate()
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index be2ad21..d279bd5 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -852,11 +852,9 @@
         final int pointerIdBits = (1 << pointerId);
         if (mSendHoverEnterAndMoveDelayed.isPending()) {
             // If we have not delivered the enter schedule an exit.
-            if (Flags.resetHoverEventTimerOnActionUp()) {
-                // We cancel first to reset the time window so that the user has the full amount of
-                // time to do a multi tap.
-                mSendHoverEnterAndMoveDelayed.repost();
-            }
+            // We cancel first to reset the time window so that the user has the full amount of
+            // time to do a multi tap.
+            mSendHoverEnterAndMoveDelayed.repost();
             mSendHoverExitDelayed.post(event, rawEvent, pointerIdBits, policyFlags);
         } else {
             // The user is touch exploring so we send events for end.
@@ -1601,7 +1599,7 @@
                                 + " pointers down.");
                 return;
             }
-            if (Flags.resetHoverEventTimerOnActionUp() && mEvents.size() == 0) {
+            if (mEvents.size() == 0) {
                 return;
             }
             // Send an accessibility event to announce the touch exploration start.
diff --git a/services/autofill/bugfixes.aconfig b/services/autofill/bugfixes.aconfig
index ced10fb..70ecc05 100644
--- a/services/autofill/bugfixes.aconfig
+++ b/services/autofill/bugfixes.aconfig
@@ -41,3 +41,10 @@
   description: "Use weak reference to address binder leak problem"
   bug: "307972253"
 }
+
+flag {
+  name: "include_last_focused_id_and_session_id_in_client_state"
+  namespace: "autofill"
+  description: "Include the current view id and session id into the FillEventHistory as part of ClientState"
+  bug: "334141398"
+}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3f3ff4a..3a38406 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -5188,11 +5188,13 @@
                     String[] exception =  resultData.getStringArray(
                             CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION);
                     if (exception != null && exception.length >= 2) {
+                        String errType = exception[0];
+                        String errMsg = exception[1];
                         Slog.w(TAG, "Credman bottom sheet from pinned "
-                                + "entry failed with: + " + exception[0] + " , "
-                                + exception[1]);
+                                + "entry failed with: + " + errType + " , "
+                                + errMsg);
                         sendCredentialManagerResponseToApp(/*response=*/ null,
-                                new GetCredentialException(exception[0], exception[1]),
+                                new GetCredentialException(errType, errMsg),
                                 mAutofillId);
                     }
                 } else {
diff --git a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
index cfb7f337..af49df6 100644
--- a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
+++ b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
@@ -56,6 +56,7 @@
 import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.CollectionUtils;
 import com.android.server.companion.association.AssociationStore;
 
 import java.io.PrintWriter;
@@ -1031,6 +1032,9 @@
     public void sendDevicePresenceEventOnUnlocked(int userId) {
         final List<DevicePresenceEvent> deviceEvents = getPendingDevicePresenceEventsByUserId(
                 userId);
+        if (CollectionUtils.isEmpty(deviceEvents)) {
+            return;
+        }
         final List<ObservableUuid> observableUuids =
                 mObservableUuidStore.getObservableUuidsForUser(userId);
         // Notify and bind the app after the phone is unlocked.
@@ -1068,7 +1072,7 @@
             }
         }
 
-        clearPendingDevicePresenceEventsByUserId(userId);
+        removePendingDevicePresenceEventsByUserId(userId);
     }
 
     private List<DevicePresenceEvent> getPendingDevicePresenceEventsByUserId(int userId) {
@@ -1077,9 +1081,11 @@
         }
     }
 
-    private void clearPendingDevicePresenceEventsByUserId(int userId) {
+    private void removePendingDevicePresenceEventsByUserId(int userId) {
         synchronized (mPendingDevicePresenceEvents) {
-            mPendingDevicePresenceEvents.get(userId).clear();
+            if (mPendingDevicePresenceEvents.contains(userId)) {
+                mPendingDevicePresenceEvents.remove(userId);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 5ec6b72..ee5d49b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9189,6 +9189,11 @@
     private class MyBinderProxyCountEventListener implements BinderProxyCountEventListener {
         @Override
         public void onLimitReached(int uid) {
+            // Spawn a new thread for the dump as it'll take long time.
+            new Thread(() -> handleLimitReached(uid), "BinderProxy Dump: " + uid).start();
+        }
+
+        private void handleLimitReached(int uid) {
             Slog.wtf(TAG, "Uid " + uid + " sent too many Binders to uid "
                     + Process.myUid());
             BinderProxy.dumpProxyDebugInfo();
diff --git a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
index bd73cb6..1938642 100644
--- a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
@@ -23,9 +23,15 @@
 
 import android.app.Notification;
 import android.app.NotificationChannel;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.LoggingOnly;
 import android.content.Context;
 import android.media.AudioAttributes;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.util.Slog;
+import com.android.internal.compat.IPlatformCompat;
 
 /**
  * Stores the latest notification channel information for this notification
@@ -34,14 +40,26 @@
     private static final String TAG = "ChannelExtractor";
     private static final boolean DBG = false;
 
+    /**
+     * Corrects audio attributes for notifications based on characteristics of the notifications.
+     */
+    @ChangeId
+    @LoggingOnly
+    static final long RESTRICT_AUDIO_ATTRIBUTES = 331793339L;
+
     private RankingConfig mConfig;
     private Context mContext;
+    private IPlatformCompat mPlatformCompat;
 
     public void initialize(Context ctx, NotificationUsageStats usageStats) {
         mContext = ctx;
         if (DBG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
     }
 
+    public void setCompatChangeLogger(IPlatformCompat platformCompat) {
+        mPlatformCompat = platformCompat;
+    }
+
     public RankingReconsideration process(NotificationRecord record) {
         if (record == null || record.getNotification() == null) {
             if (DBG) Slog.d(TAG, "skipping empty notification");
@@ -80,6 +98,7 @@
             }
 
             if (updateAttributes) {
+                reportAudioAttributesChanged(record.getUid());
                 NotificationChannel clone = record.getChannel().copy();
                 clone.setSound(clone.getSound(), new AudioAttributes.Builder(attributes)
                         .setUsage(USAGE_NOTIFICATION)
@@ -91,6 +110,17 @@
         return null;
     }
 
+    private void reportAudioAttributesChanged(int uid) {
+        final long id = Binder.clearCallingIdentity();
+        try {
+            mPlatformCompat.reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, uid);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Unexpected exception while reporting to changecompat", e);
+        } finally {
+            Binder.restoreCallingIdentity(id);
+        }
+    }
+
     @Override
     public void setConfig(RankingConfig config) {
         mConfig = config;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 8075ae0..b48cad2 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -220,6 +220,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.LauncherApps;
+import android.content.pm.ModuleInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManagerInternal;
@@ -675,6 +676,10 @@
 
     private static final int DB_VERSION = 1;
 
+
+    private static final String ADSERVICES_MODULE_PKG_NAME =
+            "com.android.adservices";
+
     private static final String TAG_NOTIFICATION_POLICY = "notification-policy";
     private static final String ATTR_VERSION = "version";
 
@@ -736,6 +741,8 @@
 
     private AppOpsManager.OnOpChangedListener mAppOpsListener;
 
+    private ModuleInfo mAdservicesModuleInfo;
+
     static class Archive {
         final SparseArray<Boolean> mEnabled;
         final int mBufferSize;
@@ -2508,12 +2515,8 @@
                 mAppOps,
                 mUserProfiles,
                 mShowReviewPermissionsNotification);
-        mRankingHelper = new RankingHelper(getContext(),
-                mRankingHandler,
-                mPreferencesHelper,
-                mZenModeHelper,
-                mUsageStats,
-                extractorNames);
+        mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper,
+                mZenModeHelper, mUsageStats, extractorNames, mPlatformCompat);
         mSnoozeHelper = snoozeHelper;
         mGroupHelper = groupHelper;
         mHistoryManager = historyManager;
@@ -2938,6 +2941,15 @@
                 mZenModeHelper.setDeviceEffectsApplier(
                         new DefaultDeviceEffectsApplier(getContext()));
             }
+            List<ModuleInfo> moduleInfoList =
+            mPackageManagerClient.getInstalledModules(
+                PackageManager.MATCH_DEBUG_TRIAGED_MISSING);
+            // Cache adservices module info
+            for (ModuleInfo mi : moduleInfoList) {
+                if (Objects.equals(mi.getApexModuleName(), ADSERVICES_MODULE_PKG_NAME)) {
+                    mAdservicesModuleInfo = mi;
+                }
+            }
         } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
             mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis());
         } else if (phase == SystemService.PHASE_DEVICE_SPECIFIC_SERVICES_READY) {
@@ -7687,13 +7699,27 @@
     private boolean canBeNonDismissible(ApplicationInfo ai, Notification notification) {
         return notification.isMediaNotification() || isEnterpriseExempted(ai)
                 || notification.isStyle(Notification.CallStyle.class)
-                || isDefaultSearchSelectorPackage(ai.packageName);
+                || isDefaultSearchSelectorPackage(ai.packageName)
+                || isDefaultAdservicesPackage(ai.packageName);
     }
 
     private boolean isDefaultSearchSelectorPackage(String pkg) {
         return Objects.equals(mDefaultSearchSelectorPkg, pkg);
     }
 
+    private boolean isDefaultAdservicesPackage(String pkg) {
+        if (mAdservicesModuleInfo == null) {
+            return false;
+        }
+        // Handles the special package structure for mainline modules
+        for (String apkName : mAdservicesModuleInfo.getApkInApexPackageNames()) {
+            if (Objects.equals(apkName, pkg)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private boolean isEnterpriseExempted(ApplicationInfo ai) {
         // Check if the app is an organization admin app
         // TODO(b/234609037): Replace with new DPM APIs to check if organization admin
diff --git a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
index 24c1d59..f0358d1 100644
--- a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
@@ -17,6 +17,7 @@
 package com.android.server.notification;
 
 import android.content.Context;
+import com.android.internal.compat.IPlatformCompat;
 
 /**
  * Extracts signals that will be useful to the {@link NotificationComparator} and caches them
@@ -52,4 +53,6 @@
      *               DND.
      */
     void setZenHelper(ZenModeHelper helper);
+
+    default void setCompatChangeLogger(IPlatformCompat platformCompat){};
 }
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 50ca984..461bd9c 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -1387,8 +1387,7 @@
     public void updateFixedImportance(List<UserInfo> users) {
         for (UserInfo user : users) {
             List<PackageInfo> packages = mPm.getInstalledPackagesAsUser(
-                    PackageManager.PackageInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY),
-                    user.getUserHandle().getIdentifier());
+                    0, user.getUserHandle().getIdentifier());
             for (PackageInfo pi : packages) {
                 boolean fixed = mPermissionHelper.isPermissionFixed(
                         pi.packageName, user.getUserHandle().getIdentifier());
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index 68e0eaa..7756801 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -15,6 +15,9 @@
  */
 package com.android.server.notification;
 
+import static android.app.Flags.restrictAudioAttributesAlarm;
+import static android.app.Flags.restrictAudioAttributesCall;
+import static android.app.Flags.restrictAudioAttributesMedia;
 import static android.app.Flags.sortSectionByTime;
 import static android.app.NotificationManager.IMPORTANCE_MIN;
 import static android.text.TextUtils.formatSimple;
@@ -27,6 +30,7 @@
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.internal.compat.IPlatformCompat;
 import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 import com.android.tools.r8.keepanno.annotations.KeepTarget;
 import com.android.tools.r8.keepanno.annotations.UsesReflection;
@@ -56,7 +60,8 @@
                         methodName = "<init>")
             })
     public RankingHelper(Context context, RankingHandler rankingHandler, RankingConfig config,
-            ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames) {
+            ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames,
+            IPlatformCompat platformCompat) {
         mContext = context;
         mRankingHandler = rankingHandler;
         if (sortSectionByTime()) {
@@ -75,6 +80,10 @@
                 extractor.initialize(mContext, usageStats);
                 extractor.setConfig(config);
                 extractor.setZenHelper(zenHelper);
+                if (restrictAudioAttributesAlarm() || restrictAudioAttributesMedia()
+                        || restrictAudioAttributesCall()) {
+                    extractor.setCompatChangeLogger(platformCompat);
+                }
                 mSignalExtractors[i] = extractor;
             } catch (ClassNotFoundException e) {
                 Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 4c653f6..fe9c3f2 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -50,6 +50,7 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.IShortcutService;
 import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.ShortcutChangeCallback;
 import android.content.pm.LauncherApps.ShortcutQuery;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -151,6 +152,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Consumer;
@@ -320,12 +322,11 @@
 
     private final Handler mHandler;
 
-    @GuardedBy("mLock")
-    private final ArrayList<ShortcutChangeListener> mListeners = new ArrayList<>(1);
+    private final CopyOnWriteArrayList<ShortcutChangeListener> mListeners =
+            new CopyOnWriteArrayList<>();
 
-    @GuardedBy("mLock")
-    private final ArrayList<LauncherApps.ShortcutChangeCallback> mShortcutChangeCallbacks =
-            new ArrayList<>(1);
+    private final CopyOnWriteArrayList<ShortcutChangeCallback> mShortcutChangeCallbacks =
+            new CopyOnWriteArrayList<>();
 
     private final AtomicLong mRawLastResetTime = new AtomicLong(0);
 
@@ -1841,18 +1842,11 @@
             @UserIdInt final int userId) {
         return () -> {
             try {
-                final ArrayList<ShortcutChangeListener> copy;
-                synchronized (mLock) {
-                    if (!isUserUnlockedL(userId)) {
-                        return;
-                    }
-
-                    copy = new ArrayList<>(mListeners);
+                if (!isUserUnlockedL(userId)) {
+                    return;
                 }
                 // Note onShortcutChanged() needs to be called with the system service permissions.
-                for (int i = copy.size() - 1; i >= 0; i--) {
-                    copy.get(i).onShortcutChanged(packageName, userId);
-                }
+                mListeners.forEach(listener -> listener.onShortcutChanged(packageName, userId));
             } catch (Exception ignore) {
             }
         };
@@ -1867,22 +1861,17 @@
         final UserHandle user = UserHandle.of(userId);
         injectPostToHandler(() -> {
             try {
-                final ArrayList<LauncherApps.ShortcutChangeCallback> copy;
-                synchronized (mLock) {
-                    if (!isUserUnlockedL(userId)) {
-                        return;
-                    }
-
-                    copy = new ArrayList<>(mShortcutChangeCallbacks);
+                if (!isUserUnlockedL(userId)) {
+                    return;
                 }
-                for (int i = copy.size() - 1; i >= 0; i--) {
+                mShortcutChangeCallbacks.forEach(callback -> {
                     if (!CollectionUtils.isEmpty(changedList)) {
-                        copy.get(i).onShortcutsAddedOrUpdated(packageName, changedList, user);
+                        callback.onShortcutsAddedOrUpdated(packageName, changedList, user);
                     }
                     if (!CollectionUtils.isEmpty(removedList)) {
-                        copy.get(i).onShortcutsRemoved(packageName, removedList, user);
+                        callback.onShortcutsRemoved(packageName, removedList, user);
                     }
-                }
+                });
             } catch (Exception ignore) {
             }
         });
@@ -3425,17 +3414,13 @@
 
         @Override
         public void addListener(@NonNull ShortcutChangeListener listener) {
-            synchronized (mLock) {
-                mListeners.add(Objects.requireNonNull(listener));
-            }
+            mListeners.add(Objects.requireNonNull(listener));
         }
 
         @Override
         public void addShortcutChangeCallback(
                 @NonNull LauncherApps.ShortcutChangeCallback callback) {
-            synchronized (mLock) {
-                mShortcutChangeCallbacks.add(Objects.requireNonNull(callback));
-            }
+            mShortcutChangeCallbacks.add(Objects.requireNonNull(callback));
         }
 
         @Override
diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
index c5d3333..3619253 100644
--- a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
+++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
@@ -64,12 +64,19 @@
 
     private static final int PACKET_LOSS_PERCENT_UNAVAILABLE = -1;
 
+    // Ignore the packet loss detection result if the expected packet number is smaller than 10.
+    // Solarwinds NPM uses 10 ICMP echos to calculate packet loss rate (as per
+    // https://thwack.solarwinds.com/products/network-performance-monitor-npm/f/forum/63829/how-is-packet-loss-calculated)
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int MIN_VALID_EXPECTED_RX_PACKET_NUM = 10;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(
             prefix = {"PACKET_LOSS_"},
             value = {
                 PACKET_LOSS_RATE_VALID,
                 PACKET_LOSS_RATE_INVALID,
+                PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP,
             })
     @Target({ElementType.TYPE_USE})
     private @interface PacketLossResultType {}
@@ -84,11 +91,23 @@
      * <ul>
      *   <li>The replay window did not proceed and thus all packets might have been delivered out of
      *       order
+     *   <li>The expected received packet number is too small and thus the detection result is not
+     *       reliable
      *   <li>There are unexpected errors
      * </ul>
      */
     private static final int PACKET_LOSS_RATE_INVALID = 1;
 
+    /**
+     * The sequence number increase is unusually large and might be caused an intentional leap on
+     * the server's downlink
+     *
+     * <p>Inbound sequence number will not always increase consecutively. During load balancing the
+     * server might add a big leap on the sequence number intentionally. In such case a high packet
+     * loss rate does not always indicate a lossy network
+     */
+    private static final int PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP = 2;
+
     // For VoIP, losses between 5% and 10% of the total packet stream will affect the quality
     // significantly (as per "Computer Networking for LANS to WANS: Hardware, Software and
     // Security"). For audio and video streaming, above 10-12% packet loss is unacceptable (as per
@@ -98,8 +117,12 @@
 
     private static final int POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT = 20;
 
+    // By default, there's no maximum limit enforced
+    private static final int MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED = -1;
+
     private long mPollIpSecStateIntervalMs;
-    private final int mPacketLossRatePercentThreshold;
+    private int mPacketLossRatePercentThreshold;
+    private int mMaxSeqNumIncreasePerSecond;
 
     @NonNull private final Handler mHandler;
     @NonNull private final PowerManager mPowerManager;
@@ -138,6 +161,7 @@
 
         mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
         mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig);
+        mMaxSeqNumIncreasePerSecond = getMaxSeqNumIncreasePerSecond(carrierConfig);
 
         // Register for system broadcasts to monitor idle mode change
         final IntentFilter intentFilter = new IntentFilter();
@@ -202,6 +226,24 @@
         return IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT;
     }
 
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static int getMaxSeqNumIncreasePerSecond(@Nullable PersistableBundleWrapper carrierConfig) {
+        int maxSeqNumIncrease = MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED;
+        if (Flags.handleSeqNumLeap() && carrierConfig != null) {
+            maxSeqNumIncrease =
+                    carrierConfig.getInt(
+                            VcnManager.VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY,
+                            MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED);
+        }
+
+        if (maxSeqNumIncrease < MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED) {
+            logE(TAG, "Invalid value of MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY " + maxSeqNumIncrease);
+            return MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED;
+        }
+
+        return maxSeqNumIncrease;
+    }
+
     @Override
     protected void onSelectedUnderlyingNetworkChanged() {
         if (!isSelectedUnderlyingNetwork()) {
@@ -237,6 +279,11 @@
         // The already scheduled event will not be affected. The followup events will be scheduled
         // with the new interval
         mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
+
+        if (Flags.handleSeqNumLeap()) {
+            mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig);
+            mMaxSeqNumIncreasePerSecond = getMaxSeqNumIncreasePerSecond(carrierConfig);
+        }
     }
 
     @Override
@@ -339,7 +386,10 @@
 
         final PacketLossCalculationResult calculateResult =
                 mPacketLossCalculator.getPacketLossRatePercentage(
-                        mLastIpSecTransformState, state, getLogPrefix());
+                        mLastIpSecTransformState,
+                        state,
+                        mMaxSeqNumIncreasePerSecond,
+                        getLogPrefix());
 
         if (calculateResult.getResultType() == PACKET_LOSS_RATE_INVALID) {
             return;
@@ -356,11 +406,18 @@
         mLastIpSecTransformState = state;
         if (calculateResult.getPacketLossRatePercent() < mPacketLossRatePercentThreshold) {
             logV(logMsg);
+
+            // In both "valid" or "unusual_seq_num_leap" cases, notify that the network has passed
+            // the validation
             onValidationResultReceivedInternal(false /* isFailed */);
         } else {
             logInfo(logMsg);
-            onValidationResultReceivedInternal(true /* isFailed */);
 
+            if (calculateResult.getResultType() == PACKET_LOSS_RATE_VALID) {
+                onValidationResultReceivedInternal(true /* isFailed */);
+            }
+
+            // In both "valid" or "unusual_seq_num_leap" cases, trigger network validation
             if (Flags.validateNetworkOnIpsecLoss()) {
                 // Trigger re-validation of the underlying network; if it fails, the VCN will
                 // attempt to migrate away.
@@ -376,6 +433,7 @@
         public PacketLossCalculationResult getPacketLossRatePercentage(
                 @NonNull IpSecTransformState oldState,
                 @NonNull IpSecTransformState newState,
+                int maxSeqNumIncreasePerSecond,
                 String logPrefix) {
             logVIpSecTransform("oldState", oldState, logPrefix);
             logVIpSecTransform("newState", newState, logPrefix);
@@ -392,6 +450,22 @@
                 return PacketLossCalculationResult.invalid();
             }
 
+            boolean isUnusualSeqNumLeap = false;
+
+            // Handle sequence number leap
+            if (Flags.handleSeqNumLeap()
+                    && maxSeqNumIncreasePerSecond != MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED) {
+                final long timeDiffMillis =
+                        newState.getTimestampMillis() - oldState.getTimestampMillis();
+                final long maxSeqNumIncrease = timeDiffMillis * maxSeqNumIncreasePerSecond / 1000;
+
+                // Sequence numbers are unsigned 32-bit values. If maxSeqNumIncrease overflows,
+                // isUnusualSeqNumLeap can never be true.
+                if (maxSeqNumIncrease >= 0 && newSeqHi - oldSeqHi >= maxSeqNumIncrease) {
+                    isUnusualSeqNumLeap = true;
+                }
+            }
+
             // Get the expected packet count by assuming there is no packet loss. In this case, SA
             // should receive all packets whose sequence numbers are smaller than the lower bound of
             // the replay window AND the packets received within the window.
@@ -411,6 +485,11 @@
                             + " actualPktCntDiff: "
                             + actualPktCntDiff);
 
+            if (Flags.handleSeqNumLeap() && expectedPktCntDiff < MIN_VALID_EXPECTED_RX_PACKET_NUM) {
+                // The sample size is too small to ensure a reliable detection result
+                return PacketLossCalculationResult.invalid();
+            }
+
             if (expectedPktCntDiff < 0
                     || expectedPktCntDiff == 0
                     || actualPktCntDiff < 0
@@ -420,7 +499,9 @@
             }
 
             final int percent = 100 - (int) (actualPktCntDiff * 100 / expectedPktCntDiff);
-            return PacketLossCalculationResult.valid(percent);
+            return isUnusualSeqNumLeap
+                    ? PacketLossCalculationResult.unusualSeqNumLeap(percent)
+                    : PacketLossCalculationResult.valid(percent);
         }
     }
 
@@ -462,6 +543,11 @@
                     PACKET_LOSS_RATE_INVALID, PACKET_LOSS_PERCENT_UNAVAILABLE);
         }
 
+        /** Construct an instance indicating that there is an unusual sequence number leap */
+        public static PacketLossCalculationResult unusualSeqNumLeap(int percent) {
+            return new PacketLossCalculationResult(PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP, percent);
+        }
+
         @PacketLossResultType
         public int getResultType() {
             return mResultType;
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
index a1b212f..b9b1060 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
@@ -272,6 +272,11 @@
         }
     }
 
+    protected static void logE(String className, String msgWithPrefix) {
+        Slog.w(className, msgWithPrefix);
+        LOCAL_LOG.log("[ERROR ] " + className + msgWithPrefix);
+    }
+
     protected static void logWtf(String className, String msgWithPrefix) {
         Slog.wtf(className, msgWithPrefix);
         LOCAL_LOG.log("[WTF ] " + className + msgWithPrefix);
diff --git a/services/core/jni/linux/usb/f_accessory.h b/services/core/jni/linux/usb/f_accessory.h
new file mode 100644
index 0000000..abd864c
--- /dev/null
+++ b/services/core/jni/linux/usb/f_accessory.h
@@ -0,0 +1,34 @@
+/*
+ * This file is auto-generated. Modifications will be lost.
+ *
+ * See https://android.googlesource.com/platform/bionic/+/master/libc/kernel/
+ * for more information.
+ */
+#ifndef _UAPI_LINUX_USB_F_ACCESSORY_H
+#define _UAPI_LINUX_USB_F_ACCESSORY_H
+#define USB_ACCESSORY_VENDOR_ID 0x18D1
+#define USB_ACCESSORY_PRODUCT_ID 0x2D00
+#define USB_ACCESSORY_ADB_PRODUCT_ID 0x2D01
+#define ACCESSORY_STRING_MANUFACTURER 0
+#define ACCESSORY_STRING_MODEL 1
+#define ACCESSORY_STRING_DESCRIPTION 2
+#define ACCESSORY_STRING_VERSION 3
+#define ACCESSORY_STRING_URI 4
+#define ACCESSORY_STRING_SERIAL 5
+#define ACCESSORY_GET_PROTOCOL 51
+#define ACCESSORY_SEND_STRING 52
+#define ACCESSORY_START 53
+#define ACCESSORY_REGISTER_HID 54
+#define ACCESSORY_UNREGISTER_HID 55
+#define ACCESSORY_SET_HID_REPORT_DESC 56
+#define ACCESSORY_SEND_HID_EVENT 57
+#define ACCESSORY_SET_AUDIO_MODE 58
+#define ACCESSORY_GET_STRING_MANUFACTURER _IOW('M', 1, char[256])
+#define ACCESSORY_GET_STRING_MODEL _IOW('M', 2, char[256])
+#define ACCESSORY_GET_STRING_DESCRIPTION _IOW('M', 3, char[256])
+#define ACCESSORY_GET_STRING_VERSION _IOW('M', 4, char[256])
+#define ACCESSORY_GET_STRING_URI _IOW('M', 5, char[256])
+#define ACCESSORY_GET_STRING_SERIAL _IOW('M', 6, char[256])
+#define ACCESSORY_IS_START_REQUESTED _IO('M', 7)
+#define ACCESSORY_GET_AUDIO_MODE _IO('M', 8)
+#endif
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index dedb687..b1673e2 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -26,6 +26,7 @@
 import android.credentials.CredentialProviderInfo;
 import android.credentials.GetCandidateCredentialsException;
 import android.credentials.GetCandidateCredentialsResponse;
+import android.credentials.GetCredentialException;
 import android.credentials.GetCredentialRequest;
 import android.credentials.GetCredentialResponse;
 import android.credentials.IGetCandidateCredentialsCallback;
@@ -159,24 +160,26 @@
     public void onFinalErrorReceived(ComponentName componentName, String errorType,
             String message) {
         Slog.d(TAG, "onFinalErrorReceived");
+        if (GetCredentialException.TYPE_USER_CANCELED.equals(errorType)) {
+            Slog.d(TAG, "User canceled but session is not being terminated");
+            return;
+        }
         respondToFinalReceiverWithFailureAndFinish(errorType, message);
     }
 
     @Override
     public void onUiCancellation(boolean isUserCancellation) {
-        String exception = GetCandidateCredentialsException.TYPE_USER_CANCELED;
-        String message = "User cancelled the selector";
-        if (!isUserCancellation) {
-            exception = GetCandidateCredentialsException.TYPE_INTERRUPTED;
-            message = "The UI was interrupted - please try again.";
-        }
-        mRequestSessionMetric.collectFrameworkException(exception);
-        respondToFinalReceiverWithFailureAndFinish(exception, message);
+        Slog.d(TAG, "User canceled but session is not being terminated");
     }
 
     private void respondToFinalReceiverWithFailureAndFinish(
             String exception, String message
     ) {
+        if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
+            Slog.w(TAG, "Request has already been completed. This is strange.");
+            return;
+        }
+
         if (mAutofillCallback != null) {
             Bundle resultData = new Bundle();
             resultData.putStringArray(
@@ -221,6 +224,19 @@
     public void onFinalResponseReceived(ComponentName componentName,
             GetCredentialResponse response) {
         Slog.d(TAG, "onFinalResponseReceived");
+        if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
+            Slog.w(TAG, "Request has already been completed. This is strange.");
+            return;
+        }
+        respondToFinalReceiverWithResponseAndFinish(response);
+    }
+
+    private void respondToFinalReceiverWithResponseAndFinish(GetCredentialResponse response) {
+        if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
+            Slog.w(TAG, "Request has already been completed. This is strange.");
+            return;
+        }
+
         if (this.mAutofillCallback != null) {
             Slog.d(TAG, "onFinalResponseReceived sending through final receiver");
             Bundle resultData = new Bundle();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
index ad25d76..770712a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
@@ -26,14 +26,18 @@
 import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
 import static android.media.AudioAttributes.USAGE_UNKNOWN;
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+import static com.android.server.notification.NotificationChannelExtractor.RESTRICT_AUDIO_ATTRIBUTES;
 import static com.google.common.truth.Truth.assertThat;
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNull;
 
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Flags;
@@ -43,12 +47,14 @@
 import android.app.Person;
 import android.media.AudioAttributes;
 import android.net.Uri;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 
+import com.android.internal.compat.IPlatformCompat;
 import com.android.server.UiServiceTestCase;
 
 import org.junit.Before;
@@ -60,6 +66,8 @@
 public class NotificationChannelExtractorTest extends UiServiceTestCase {
 
     @Mock RankingConfig mConfig;
+    @Mock
+    IPlatformCompat mPlatformCompat;
 
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
@@ -73,6 +81,7 @@
         mExtractor = new NotificationChannelExtractor();
         mExtractor.setConfig(mConfig);
         mExtractor.initialize(mContext, null);
+        mExtractor.setCompatChangeLogger(mPlatformCompat);
     }
 
     private NotificationRecord getRecord(NotificationChannel channel, Notification n) {
@@ -82,7 +91,7 @@
     }
 
     @Test
-    public void testExtractsUpdatedConversationChannel() {
+    public void testExtractsUpdatedConversationChannel() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
         final Notification n = new Notification.Builder(getContext())
                 .setContentTitle("foo")
@@ -101,7 +110,7 @@
     }
 
     @Test
-    public void testInvalidShortcutFlagEnabled_looksUpCorrectNonChannel() {
+    public void testInvalidShortcutFlagEnabled_looksUpCorrectNonChannel() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
         final Notification n = new Notification.Builder(getContext())
                 .setContentTitle("foo")
@@ -122,7 +131,7 @@
     }
 
     @Test
-    public void testInvalidShortcutFlagDisabled_looksUpCorrectChannel() {
+    public void testInvalidShortcutFlagDisabled_looksUpCorrectChannel() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
         final Notification n = new Notification.Builder(getContext())
                 .setContentTitle("foo")
@@ -143,7 +152,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_CALL)
-    public void testAudioAttributes_callStyleCanUseCallUsage() {
+    public void testAudioAttributes_callStyleCanUseCallUsage() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
         channel.setSound(Uri.EMPTY, new AudioAttributes.Builder()
                 .setUsage(USAGE_NOTIFICATION_RINGTONE)
@@ -162,11 +171,12 @@
         assertThat(mExtractor.process(r)).isNull();
         assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION_RINGTONE);
         assertThat(r.getChannel()).isEqualTo(channel);
+        verify(mPlatformCompat, never()).reportChangeByUid(anyLong(), anyInt());
     }
 
     @Test
     @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_CALL)
-    public void testAudioAttributes_nonCallStyleCannotUseCallUsage() {
+    public void testAudioAttributes_nonCallStyleCannotUseCallUsage() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
         channel.setSound(Uri.EMPTY, new AudioAttributes.Builder()
                 .setUsage(USAGE_NOTIFICATION_RINGTONE)
@@ -180,13 +190,14 @@
         assertThat(mExtractor.process(r)).isNull();
         // instance updated
         assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION);
+        verify(mPlatformCompat).reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, r.getUid());
         // in-memory channel unchanged
         assertThat(channel.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION_RINGTONE);
     }
 
     @Test
     @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_ALARM)
-    public void testAudioAttributes_alarmCategoryCanUseAlarmUsage() {
+    public void testAudioAttributes_alarmCategoryCanUseAlarmUsage() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
         channel.setSound(Uri.EMPTY, new AudioAttributes.Builder()
                 .setUsage(USAGE_ALARM)
@@ -201,11 +212,12 @@
         assertThat(mExtractor.process(r)).isNull();
         assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_ALARM);
         assertThat(r.getChannel()).isEqualTo(channel);
+        verify(mPlatformCompat, never()).reportChangeByUid(anyLong(), anyInt());
     }
 
     @Test
     @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_ALARM)
-    public void testAudioAttributes_nonAlarmCategoryCannotUseAlarmUsage() {
+    public void testAudioAttributes_nonAlarmCategoryCannotUseAlarmUsage() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
         channel.setSound(Uri.EMPTY, new AudioAttributes.Builder()
                 .setUsage(USAGE_ALARM)
@@ -219,13 +231,14 @@
         assertThat(mExtractor.process(r)).isNull();
         // instance updated
         assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION);
+        verify(mPlatformCompat).reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, r.getUid());
         // in-memory channel unchanged
         assertThat(channel.getAudioAttributes().getUsage()).isEqualTo(USAGE_ALARM);
     }
 
     @Test
     @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_MEDIA)
-    public void testAudioAttributes_noMediaUsage() {
+    public void testAudioAttributes_noMediaUsage() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
         channel.setSound(Uri.EMPTY, new AudioAttributes.Builder()
                 .setUsage(USAGE_MEDIA)
@@ -239,13 +252,14 @@
         assertThat(mExtractor.process(r)).isNull();
         // instance updated
         assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION);
+        verify(mPlatformCompat).reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, r.getUid());
         // in-memory channel unchanged
         assertThat(channel.getAudioAttributes().getUsage()).isEqualTo(USAGE_MEDIA);
     }
 
     @Test
     @EnableFlags(Flags.FLAG_RESTRICT_AUDIO_ATTRIBUTES_MEDIA)
-    public void testAudioAttributes_noUnknownUsage() {
+    public void testAudioAttributes_noUnknownUsage() throws RemoteException {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
         channel.setSound(Uri.EMPTY, new AudioAttributes.Builder()
                 .setUsage(USAGE_UNKNOWN)
@@ -259,6 +273,7 @@
         assertThat(mExtractor.process(r)).isNull();
         // instance updated
         assertThat(r.getAudioAttributes().getUsage()).isEqualTo(USAGE_NOTIFICATION);
+        verify(mPlatformCompat).reportChangeByUid(RESTRICT_AUDIO_ATTRIBUTES, r.getUid());
         // in-memory channel unchanged
         assertThat(channel.getAudioAttributes().getUsage()).isEqualTo(USAGE_UNKNOWN);
     }
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 805bc17..011f2e3 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -200,6 +200,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.LauncherApps;
+import android.content.pm.ModuleInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
@@ -450,6 +451,9 @@
 
     private static final String VALID_CONVO_SHORTCUT_ID = "shortcut";
     private static final String SEARCH_SELECTOR_PKG = "searchSelector";
+    private static final String ADSERVICES_MODULE_PKG = "com.android.adservices";
+    private static final String ADSERVICES_APK_PKG = "com.android.adservices.api";
+
     @Mock
     private NotificationListeners mListeners;
     @Mock
@@ -740,7 +744,11 @@
 
         // Return first true for RoleObserver main-thread check
         when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false);
-
+        ModuleInfo moduleInfo = new ModuleInfo();
+        moduleInfo.setApexModuleName(ADSERVICES_MODULE_PKG);
+        moduleInfo.setApkInApexPackageNames(List.of(ADSERVICES_APK_PKG));
+        when(mPackageManagerClient.getInstalledModules(anyInt()))
+                .thenReturn(List.of(moduleInfo));
         if (upToBootPhase >= SystemService.PHASE_SYSTEM_SERVICES_READY) {
             mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY, mMainLooper);
         }
@@ -13218,6 +13226,33 @@
     }
 
     @Test
+    public void fixSystemNotification_defaultAdservices_withOnGoingFlag_nondismissible()
+            throws Exception {
+        final ApplicationInfo ai = new ApplicationInfo();
+        ai.packageName = ADSERVICES_APK_PKG;
+        ai.uid = mUid;
+        ai.flags |= ApplicationInfo.FLAG_SYSTEM;
+
+        when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(ai);
+        when(mAppOpsManager.checkOpNoThrow(
+                AppOpsManager.OP_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS, ai.uid,
+                ai.packageName)).thenReturn(AppOpsManager.MODE_IGNORED);
+        // Given: a notification from an app on the system partition has the flag
+        // FLAG_ONGOING_EVENT set
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(true)
+                .build();
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, ADSERVICES_APK_PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE,
+                 true);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should be set
+        assertNotSame(0, n.flags & Notification.FLAG_NO_DISMISS);
+    }
+
+    @Test
     public void fixCallNotification_withOnGoingFlag_shouldNotBeNonDismissible()
             throws Exception {
         // Given: a call notification has the flag FLAG_ONGOING_EVENT set
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index aeeca2ae..5033a380 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -3981,7 +3981,7 @@
         pm.applicationInfo = new ApplicationInfo();
         pm.applicationInfo.uid = UID_O;
         List<PackageInfo> packages = ImmutableList.of(pm);
-        when(mPm.getInstalledPackagesAsUser(any(), anyInt())).thenReturn(packages);
+        when(mPm.getInstalledPackagesAsUser(eq(0), anyInt())).thenReturn(packages);
         mHelper.updateFixedImportance(users);
 
         assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O));
@@ -4097,7 +4097,7 @@
         pm.applicationInfo = new ApplicationInfo();
         pm.applicationInfo.uid = UID_O;
         List<PackageInfo> packages = ImmutableList.of(pm);
-        when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages);
+        when(mPm.getInstalledPackagesAsUser(0, 0)).thenReturn(packages);
         mHelper.updateFixedImportance(users);
 
         assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false)
@@ -4120,7 +4120,7 @@
         pm.applicationInfo = new ApplicationInfo();
         pm.applicationInfo.uid = UID_O;
         List<PackageInfo> packages = ImmutableList.of(pm);
-        when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages);
+        when(mPm.getInstalledPackagesAsUser(0, 0)).thenReturn(packages);
         mHelper.updateFixedImportance(users);
 
         NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
@@ -4309,7 +4309,7 @@
         pm.applicationInfo = new ApplicationInfo();
         pm.applicationInfo.uid = UID_O;
         List<PackageInfo> packages = ImmutableList.of(pm);
-        when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages);
+        when(mPm.getInstalledPackagesAsUser(0, 0)).thenReturn(packages);
         mHelper.updateFixedImportance(users);
 
         ArraySet<String> toRemove = new ArraySet<>();
@@ -4341,7 +4341,7 @@
         pm.applicationInfo = new ApplicationInfo();
         pm.applicationInfo.uid = UID_O;
         List<PackageInfo> packages = ImmutableList.of(pm);
-        when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages);
+        when(mPm.getInstalledPackagesAsUser(0, 0)).thenReturn(packages);
         mHelper.updateFixedImportance(users);
 
         assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O));
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index ad420f6..527001d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -55,6 +55,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.compat.IPlatformCompat;
 import com.android.server.UiServiceTestCase;
 
 import org.junit.Before;
@@ -155,7 +156,8 @@
                 NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0);
         when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
         mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper,
-                mUsageStats, new String[] {ImportanceExtractor.class.getName()});
+                mUsageStats, new String[] {ImportanceExtractor.class.getName()},
+                mock(IPlatformCompat.class));
 
         mNotiGroupGSortA = new Notification.Builder(mContext, TEST_CHANNEL_ID)
                 .setContentTitle("A")
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
index 0a83a53..c8b60e5 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
@@ -17,8 +17,11 @@
 package com.android.server.vcn.routeselection;
 
 import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY;
+import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY;
 import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY;
 
+import static com.android.server.vcn.routeselection.IpSecPacketLossDetector.MIN_VALID_EXPECTED_RX_PACKET_NUM;
+import static com.android.server.vcn.routeselection.IpSecPacketLossDetector.getMaxSeqNumIncreasePerSecond;
 import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertEquals;
@@ -65,6 +68,7 @@
     private static final int REPLAY_BITMAP_LEN_BYTE = 512;
     private static final int REPLAY_BITMAP_LEN_BIT = REPLAY_BITMAP_LEN_BYTE * 8;
     private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD = 5;
+    private static final int MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED = -1;
     private static final long POLL_IPSEC_STATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(30L);
 
     @Mock private IpSecTransformWrapper mIpSecTransform;
@@ -91,6 +95,9 @@
                         eq(VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY),
                         anyInt()))
                 .thenReturn(IPSEC_PACKET_LOSS_PERCENT_THRESHOLD);
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY), anyInt()))
+                .thenReturn(MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED);
 
         when(mDependencies.getPacketLossCalculator()).thenReturn(mPacketLossCalculator);
 
@@ -112,6 +119,20 @@
                 .build();
     }
 
+    private static IpSecTransformState newNextTransformState(
+            IpSecTransformState before,
+            long timeDiffMillis,
+            long rxSeqNoDiff,
+            long packtCountDiff,
+            int packetInWin) {
+        return new IpSecTransformState.Builder()
+                .setTimestampMillis(before.getTimestampMillis() + timeDiffMillis)
+                .setRxHighestSequenceNumber(before.getRxHighestSequenceNumber() + rxSeqNoDiff)
+                .setPacketCount(before.getPacketCount() + packtCountDiff)
+                .setReplayBitmap(newReplayBitmap(packetInWin))
+                .build();
+    }
+
     private static byte[] newReplayBitmap(int receivedPktCnt) {
         final BitSet bitSet = new BitSet(REPLAY_BITMAP_LEN_BIT);
         for (int i = 0; i < receivedPktCnt; i++) {
@@ -165,7 +186,7 @@
         // Verify the first polled state is stored
         assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
         verify(mPacketLossCalculator, never())
-                .getPacketLossRatePercentage(any(), any(), anyString());
+                .getPacketLossRatePercentage(any(), any(), anyInt(), anyString());
 
         // Verify next poll is scheduled
         assertNull(mTestLooper.nextMessage());
@@ -278,7 +299,7 @@
 
         xfrmStateReceiver.onResult(newTransformState(1, 1, newReplayBitmap(1)));
         verify(mPacketLossCalculator, never())
-                .getPacketLossRatePercentage(any(), any(), anyString());
+                .getPacketLossRatePercentage(any(), any(), anyInt(), anyString());
     }
 
     @Test
@@ -289,7 +310,7 @@
 
         xfrmStateReceiver.onError(new RuntimeException("Test"));
         verify(mPacketLossCalculator, never())
-                .getPacketLossRatePercentage(any(), any(), anyString());
+                .getPacketLossRatePercentage(any(), any(), anyInt(), anyString());
     }
 
     private void checkHandleLossRate(
@@ -301,7 +322,7 @@
                 startMonitorAndCaptureStateReceiver();
         doReturn(mockPacketLossRate)
                 .when(mPacketLossCalculator)
-                .getPacketLossRatePercentage(any(), any(), anyString());
+                .getPacketLossRatePercentage(any(), any(), anyInt(), anyString());
 
         // Mock receiving two states with mTransformStateInitial and an arbitrary transformNew
         final IpSecTransformState transformNew = newTransformState(1, 1, newReplayBitmap(1));
@@ -311,7 +332,10 @@
         // Verifications
         verify(mPacketLossCalculator)
                 .getPacketLossRatePercentage(
-                        eq(mTransformStateInitial), eq(transformNew), anyString());
+                        eq(mTransformStateInitial),
+                        eq(transformNew),
+                        eq(MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED),
+                        anyString());
 
         if (isLastStateExpectedToUpdate) {
             assertEquals(transformNew, mIpSecPacketLossDetector.getLastTransformState());
@@ -351,6 +375,22 @@
                 false /* isCallbackExpected */);
     }
 
+    @Test
+    public void testHandleLossRate_unusualSeqNumLeap_highLossRate() throws Exception {
+        checkHandleLossRate(
+                PacketLossCalculationResult.unusualSeqNumLeap(22),
+                true /* isLastStateExpectedToUpdate */,
+                false /* isCallbackExpected */);
+    }
+
+    @Test
+    public void testHandleLossRate_unusualSeqNumLeap_lowLossRate() throws Exception {
+        checkHandleLossRate(
+                PacketLossCalculationResult.unusualSeqNumLeap(2),
+                true /* isLastStateExpectedToUpdate */,
+                true /* isCallbackExpected */);
+    }
+
     private void checkGetPacketLossRate(
             IpSecTransformState oldState,
             IpSecTransformState newState,
@@ -358,7 +398,8 @@
             throws Exception {
         assertEquals(
                 expectedLossRate,
-                mPacketLossCalculator.getPacketLossRatePercentage(oldState, newState, TAG));
+                mPacketLossCalculator.getPacketLossRatePercentage(
+                        oldState, newState, MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED, TAG));
     }
 
     private void checkGetPacketLossRate(
@@ -397,6 +438,21 @@
     }
 
     @Test
+    public void testGetPacketLossRate_expectedPacketNumTooFew() throws Exception {
+        final int oldRxNo = 4096;
+        final int oldPktCnt = 4096;
+        final int pktCntDiff = MIN_VALID_EXPECTED_RX_PACKET_NUM - 1;
+        final byte[] bitmapReceiveAll = newReplayBitmap(4096);
+
+        final IpSecTransformState oldState =
+                newTransformState(oldRxNo, oldPktCnt, bitmapReceiveAll);
+        final IpSecTransformState newState =
+                newTransformState(oldRxNo + pktCntDiff, oldPktCnt + pktCntDiff, bitmapReceiveAll);
+
+        checkGetPacketLossRate(oldState, newState, PacketLossCalculationResult.invalid());
+    }
+
+    @Test
     public void testGetPacketLossRate_againstInitialState() throws Exception {
         checkGetPacketLossRate(mTransformStateInitial, 7000, 7001, 4096, 0);
         checkGetPacketLossRate(mTransformStateInitial, 7000, 6000, 4096, 15);
@@ -443,6 +499,45 @@
         checkGetPacketLossRate(oldState, 20000, 14000, 3000, 10);
     }
 
+    private void checkGetPktLossRate_unusualSeqNumLeap(
+            int maxSeqNumIncreasePerSecond,
+            int timeDiffMillis,
+            int rxSeqNoDiff,
+            PacketLossCalculationResult expected)
+            throws Exception {
+        final IpSecTransformState oldState = mTransformStateInitial;
+        final IpSecTransformState newState =
+                newNextTransformState(
+                        oldState,
+                        timeDiffMillis,
+                        rxSeqNoDiff,
+                        1 /* packtCountDiff */,
+                        1 /* packetInWin */);
+
+        assertEquals(
+                expected,
+                mPacketLossCalculator.getPacketLossRatePercentage(
+                        oldState, newState, maxSeqNumIncreasePerSecond, TAG));
+    }
+
+    @Test
+    public void testGetPktLossRate_unusualSeqNumLeap() throws Exception {
+        checkGetPktLossRate_unusualSeqNumLeap(
+                10000 /* maxSeqNumIncreasePerSecond */,
+                (int) TimeUnit.SECONDS.toMillis(2L),
+                30000 /* rxSeqNoDiff */,
+                PacketLossCalculationResult.unusualSeqNumLeap(100));
+    }
+
+    @Test
+    public void testGetPktLossRate_unusualSeqNumLeap_smallSeqNumDiff() throws Exception {
+        checkGetPktLossRate_unusualSeqNumLeap(
+                10000 /* maxSeqNumIncreasePerSecond */,
+                (int) TimeUnit.SECONDS.toMillis(2L),
+                5000 /* rxSeqNoDiff */,
+                PacketLossCalculationResult.valid(100));
+    }
+
     // Verify the polling event is scheduled with expected delays
     private void verifyPollEventDelayAndScheduleNext(long expectedDelayMs) {
         if (expectedDelayMs > 0) {
@@ -469,4 +564,24 @@
         // Verify the 3rd poll is scheduled with configured delay
         verifyPollEventDelayAndScheduleNext(POLL_IPSEC_STATE_INTERVAL_MS);
     }
+
+    @Test
+    public void testGetMaxSeqNumIncreasePerSecond() throws Exception {
+        final int seqNumLeapNegative = 500_000;
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY), anyInt()))
+                .thenReturn(seqNumLeapNegative);
+        assertEquals(seqNumLeapNegative, getMaxSeqNumIncreasePerSecond(mCarrierConfig));
+    }
+
+    @Test
+    public void testGetMaxSeqNumIncreasePerSecond_negativeValue() throws Exception {
+        final int seqNumLeapNegative = -10;
+        when(mCarrierConfig.getInt(
+                        eq(VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY), anyInt()))
+                .thenReturn(seqNumLeapNegative);
+        assertEquals(
+                MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED,
+                getMaxSeqNumIncreasePerSecond(mCarrierConfig));
+    }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
index af6daa1..6189fb0 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -123,6 +123,7 @@
 
         mSetFlagsRule.enableFlags(Flags.FLAG_VALIDATE_NETWORK_ON_IPSEC_LOSS);
         mSetFlagsRule.enableFlags(Flags.FLAG_EVALUATE_IPSEC_LOSS_ON_LP_NC_CHANGE);
+        mSetFlagsRule.enableFlags(Flags.FLAG_HANDLE_SEQ_NUM_LEAP);
 
         when(mNetwork.getNetId()).thenReturn(-1);