Merge "Add a new flag so that we can switch and avoid teamfood problems" into main
diff --git a/core/java/android/view/InsetsFlags.java b/core/java/android/view/InsetsFlags.java
index ca8a7a8..2fa5768 100644
--- a/core/java/android/view/InsetsFlags.java
+++ b/core/java/android/view/InsetsFlags.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static android.view.WindowInsetsController.APPEARANCE_FORCE_LIGHT_NAVIGATION_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
@@ -24,6 +25,7 @@
 import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS;
+import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND;
 import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 
@@ -69,7 +71,15 @@
             @ViewDebug.FlagToString(
                     mask = APPEARANCE_FORCE_LIGHT_NAVIGATION_BARS,
                     equals = APPEARANCE_FORCE_LIGHT_NAVIGATION_BARS,
-                    name = "FORCE_LIGHT_NAVIGATION_BARS")
+                    name = "FORCE_LIGHT_NAVIGATION_BARS"),
+            @ViewDebug.FlagToString(
+                    mask = APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND,
+                    equals = APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND,
+                    name = "APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND"),
+            @ViewDebug.FlagToString(
+                    mask = APPEARANCE_LIGHT_CAPTION_BARS,
+                    equals = APPEARANCE_LIGHT_CAPTION_BARS,
+                    name = "APPEARANCE_LIGHT_CAPTION_BARS")
     })
     public @Appearance int appearance;
 
diff --git a/core/java/android/view/NativeVectorDrawableAnimator.java b/core/java/android/view/NativeVectorDrawableAnimator.java
index b0556a3..e92bd1f 100644
--- a/core/java/android/view/NativeVectorDrawableAnimator.java
+++ b/core/java/android/view/NativeVectorDrawableAnimator.java
@@ -16,6 +16,8 @@
 
 package android.view;
 
+import android.animation.Animator;
+
 /**
  * Exists just to allow for android.graphics & android.view package separation
  *
@@ -26,4 +28,7 @@
 public interface NativeVectorDrawableAnimator {
     /** @hide */
     long getAnimatorNativePtr();
+
+    /** @hide */
+    void setThreadedRendererAnimatorListener(Animator.AnimatorListener listener);
 }
diff --git a/core/java/android/view/ViewAnimationHostBridge.java b/core/java/android/view/ViewAnimationHostBridge.java
index e0fae21..62b2b6c 100644
--- a/core/java/android/view/ViewAnimationHostBridge.java
+++ b/core/java/android/view/ViewAnimationHostBridge.java
@@ -16,14 +16,19 @@
 
 package android.view;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.graphics.RenderNode;
 
+import androidx.annotation.NonNull;
+
 /**
  * Maps a View to a RenderNode's AnimationHost
  *
  * @hide
  */
-public class ViewAnimationHostBridge implements RenderNode.AnimationHost {
+public class ViewAnimationHostBridge extends AnimatorListenerAdapter
+        implements RenderNode.AnimationHost {
     private final View mView;
 
     /**
@@ -34,17 +39,35 @@
     }
 
     @Override
-    public void registerAnimatingRenderNode(RenderNode animator) {
-        mView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(animator);
+    public void registerAnimatingRenderNode(RenderNode renderNode, Animator animator) {
+        mView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(renderNode);
+        animator.addListener(this);
     }
 
     @Override
     public void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animator) {
         mView.mAttachInfo.mViewRootImpl.registerVectorDrawableAnimator(animator);
+        animator.setThreadedRendererAnimatorListener(this);
     }
 
     @Override
     public boolean isAttached() {
         return mView.mAttachInfo != null;
     }
+
+    @Override
+    public void onAnimationStart(@NonNull Animator animation) {
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        if (viewRoot != null) {
+            viewRoot.addThreadedRendererView(mView);
+        }
+    }
+
+    @Override
+    public void onAnimationEnd(@NonNull Animator animation) {
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        if (viewRoot != null) {
+            viewRoot.removeThreadedRendererView(mView);
+        }
+    }
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index bdada11..139285a 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -427,6 +427,12 @@
 
     private static final long NANOS_PER_SEC = 1000000000;
 
+    // If the ViewRootImpl has been idle for more than 750ms, clear the preferred
+    // frame rate category and frame rate.
+    private static final int IDLE_TIME_MILLIS = 750;
+
+    private static final long NANOS_PER_MILLI = 1_000_000;
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
 
@@ -659,6 +665,10 @@
     private int mMinusOneFrameIntervalMillis = 0;
     // VRR interval between the previous and the frame before
     private int mMinusTwoFrameIntervalMillis = 0;
+    // VRR has the invalidation idle message been posted?
+    private boolean mInvalidationIdleMessagePosted = false;
+    // VRR: List of all Views that are animating with the threaded render
+    private ArrayList<View> mThreadedRendererViews = new ArrayList();
 
     /**
      * Update the Choreographer's FrameInfo object with the timing information for the current
@@ -1184,6 +1194,8 @@
             toolkitFrameRateVelocityMappingReadOnly();
     private static boolean sToolkitEnableInvalidateCheckThreadFlagValue =
             Flags.enableInvalidateCheckThread();
+    private static boolean sSurfaceFlingerBugfixFlagValue =
+            com.android.graphics.surfaceflinger.flags.Flags.vrrBugfix24q4();
 
     static {
         sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly();
@@ -4261,8 +4273,13 @@
         // when the values are applicable.
         if (mDrawnThisFrame) {
             mDrawnThisFrame = false;
+            if (!mInvalidationIdleMessagePosted && sSurfaceFlingerBugfixFlagValue) {
+                mInvalidationIdleMessagePosted = true;
+                mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, IDLE_TIME_MILLIS);
+            }
             setCategoryFromCategoryCounts();
             updateInfrequentCount();
+            updateFrameRateFromThreadedRendererViews();
             setPreferredFrameRate(mPreferredFrameRate);
             setPreferredFrameRateCategory(mPreferredFrameRateCategory);
             if (mPreferredFrameRate > 0
@@ -6499,6 +6516,8 @@
                     return "MSG_WINDOW_TOUCH_MODE_CHANGED";
                 case MSG_KEEP_CLEAR_RECTS_CHANGED:
                     return "MSG_KEEP_CLEAR_RECTS_CHANGED";
+                case MSG_CHECK_INVALIDATION_IDLE:
+                    return "MSG_CHECK_INVALIDATION_IDLE";
                 case MSG_REFRESH_POINTER_ICON:
                     return "MSG_REFRESH_POINTER_ICON";
                 case MSG_TOUCH_BOOST_TIMEOUT:
@@ -6759,6 +6778,31 @@
                     mNumPausedForSync = 0;
                     scheduleTraversals();
                     break;
+                case MSG_CHECK_INVALIDATION_IDLE: {
+                    long delta;
+                    if (mIsTouchBoosting || mIsFrameRateBoosting || mInsetsAnimationRunning) {
+                        delta = 0;
+                    } else {
+                        delta = System.nanoTime() / NANOS_PER_MILLI - mLastUpdateTimeMillis;
+                    }
+                    if (delta >= IDLE_TIME_MILLIS) {
+                        mFrameRateCategoryHighCount = 0;
+                        mFrameRateCategoryHighHintCount = 0;
+                        mFrameRateCategoryNormalCount = 0;
+                        mFrameRateCategoryLowCount = 0;
+                        mPreferredFrameRate = 0;
+                        mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
+                        updateFrameRateFromThreadedRendererViews();
+                        setPreferredFrameRate(mPreferredFrameRate);
+                        setPreferredFrameRateCategory(mPreferredFrameRateCategory);
+                        mInvalidationIdleMessagePosted = false;
+                    } else {
+                        mInvalidationIdleMessagePosted = true;
+                        mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE,
+                                IDLE_TIME_MILLIS - delta);
+                    }
+                    break;
+                }
                 case MSG_TOUCH_BOOST_TIMEOUT:
                     /**
                      * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME).
@@ -12581,6 +12625,24 @@
     }
 
     /**
+     * Views that are animating with the ThreadedRenderer don't use the normal invalidation
+     * path, so the value won't be updated through performTraversals. This reads the votes
+     * from those views.
+     */
+    private void updateFrameRateFromThreadedRendererViews() {
+        ArrayList<View> views = mThreadedRendererViews;
+        for (int i = views.size() - 1; i >= 0; i--) {
+            View view = views.get(i);
+            View.AttachInfo attachInfo = view.mAttachInfo;
+            if (attachInfo == null || attachInfo.mViewRootImpl != this) {
+                views.remove(i);
+            } else {
+                view.votePreferredFrameRate();
+            }
+        }
+    }
+
+    /**
      * Sets the mPreferredFrameRateCategory from the high, high_hint, normal, and low counts.
      */
     private void setCategoryFromCategoryCounts() {
@@ -12761,6 +12823,31 @@
     }
 
     /**
+     * Mark a View as having an active ThreadedRenderer animation. This is used for
+     * RenderNodeAnimators and AnimatedVectorDrawables. When the animation stops,
+     * {@link #removeThreadedRendererView(View)} must be called.
+     * @param view The View with the ThreadedRenderer animation that started.
+     */
+    public void addThreadedRendererView(View view) {
+        if (!mThreadedRendererViews.contains(view)) {
+            mThreadedRendererViews.add(view);
+        }
+    }
+
+    /**
+     * When a ThreadedRenderer animation ends, the View that is associated with it using
+     * {@link #addThreadedRendererView(View)} must be removed with a call to this method.
+     * @param view The View whose ThreadedRender animation has stopped.
+     */
+    public void removeThreadedRendererView(View view) {
+        mThreadedRendererViews.remove(view);
+        if (!mInvalidationIdleMessagePosted && sSurfaceFlingerBugfixFlagValue) {
+            mInvalidationIdleMessagePosted = true;
+            mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, IDLE_TIME_MILLIS);
+        }
+    }
+
+    /**
      * Returns {@link #INTERMITTENT_STATE_INTERMITTENT} when the ViewRootImpl has only been
      * updated intermittently, {@link #INTERMITTENT_STATE_NOT_INTERMITTENT} when it is
      * not updated intermittently, and {@link #INTERMITTENT_STATE_IN_TRANSITION} when it
@@ -12983,6 +13070,10 @@
     private void removeVrrMessages() {
         mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT);
         mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
+        if (mInvalidationIdleMessagePosted && sSurfaceFlingerBugfixFlagValue) {
+            mInvalidationIdleMessagePosted = false;
+            mHandler.removeMessages(MSG_CHECK_INVALIDATION_IDLE);
+        }
     }
 
     /**
@@ -13001,7 +13092,7 @@
         mMinusOneFrameIntervalMillis = timeIntervalMillis;
 
         mLastUpdateTimeMillis = currentTimeMillis;
-        if (timeIntervalMillis + mMinusTwoFrameIntervalMillis
+        if (mThreadedRendererViews.isEmpty() && timeIntervalMillis + mMinusTwoFrameIntervalMillis
                 >= INFREQUENT_UPDATE_INTERVAL_MILLIS) {
             int infrequentUpdateCount = mInfrequentUpdateCount;
             mInfrequentUpdateCount = infrequentUpdateCount == INFREQUENT_UPDATE_COUNTS
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 0b1b40c..07446e7 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -41,11 +41,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.platform.test.annotations.LargeTest;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.DisplayMetrics;
 import android.widget.FrameLayout;
+import android.widget.ProgressBar;
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
@@ -623,6 +625,162 @@
         assertEquals(FRAME_RATE_CATEGORY_HIGH_HINT, mViewRoot.getLastPreferredFrameRateCategory());
     }
 
+    @LargeTest
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            com.android.graphics.surfaceflinger.flags.Flags.FLAG_VRR_BUGFIX_24Q4
+    })
+    public void idleDetected() throws Throwable {
+        waitForFrameRateCategoryToSettle();
+        mActivityRule.runOnUiThread(() -> {
+            mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_HIGH);
+            mMovingView.setFrameContentVelocity(Float.MAX_VALUE);
+            mMovingView.invalidate();
+            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
+                    mViewRoot.getLastPreferredFrameRateCategory()));
+        });
+        waitForAfterDraw();
+
+        // Wait for idle timeout
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                mViewRoot.getLastPreferredFrameRateCategory());
+    }
+
+    @LargeTest
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            com.android.graphics.surfaceflinger.flags.Flags.FLAG_VRR_BUGFIX_24Q4
+    })
+    public void vectorDrawableFrameRate() throws Throwable {
+        final ProgressBar[] progressBars = new ProgressBar[3];
+        final ViewGroup[] parents = new ViewGroup[1];
+        mActivityRule.runOnUiThread(() -> {
+            ViewGroup parent = (ViewGroup) mMovingView.getParent();
+            parents[0] = parent;
+            ProgressBar progressBar1 = new ProgressBar(mActivity);
+            parent.addView(progressBar1);
+            progressBar1.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            progressBar1.setIndeterminate(true);
+            progressBars[0] = progressBar1;
+
+            ProgressBar progressBar2 = new ProgressBar(mActivity);
+            parent.addView(progressBar2);
+            progressBar2.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NORMAL);
+            progressBar2.setIndeterminate(true);
+            progressBars[1] = progressBar2;
+
+            ProgressBar progressBar3 = new ProgressBar(mActivity);
+            parent.addView(progressBar3);
+            progressBar3.setRequestedFrameRate(45f);
+            progressBar3.setIndeterminate(true);
+            progressBars[2] = progressBar3;
+        });
+        waitForFrameRateCategoryToSettle();
+
+        // Wait for idle timeout
+        Thread.sleep(1000);
+        assertEquals(45f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NORMAL, mViewRoot.getLastPreferredFrameRateCategory());
+
+        // Removing the vector drawable with NORMAL should drop the category to LOW
+        mActivityRule.runOnUiThread(() -> parents[0].removeView(progressBars[1]));
+        Thread.sleep(1000);
+        assertEquals(45f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_LOW,
+                mViewRoot.getLastPreferredFrameRateCategory());
+        // Removing the one voting for frame rate should leave only the category
+        mActivityRule.runOnUiThread(() -> parents[0].removeView(progressBars[2]));
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_LOW,
+                mViewRoot.getLastPreferredFrameRateCategory());
+        // Removing the last one should leave it with no preference
+        mActivityRule.runOnUiThread(() -> parents[0].removeView(progressBars[0]));
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                mViewRoot.getLastPreferredFrameRateCategory());
+    }
+
+    @LargeTest
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            com.android.graphics.surfaceflinger.flags.Flags.FLAG_VRR_BUGFIX_24Q4
+    })
+    public void renderNodeAnimatorFrameRateCanceled() throws Throwable {
+        mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+        waitForFrameRateCategoryToSettle();
+
+        RenderNodeAnimator[] renderNodeAnimator = new RenderNodeAnimator[1];
+        renderNodeAnimator[0] = new RenderNodeAnimator(RenderNodeAnimator.ALPHA, 0f);
+        renderNodeAnimator[0].setDuration(100000);
+
+        mActivityRule.runOnUiThread(() -> {
+            renderNodeAnimator[0].setTarget(mMovingView);
+            renderNodeAnimator[0].start();
+            mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            runAfterDraw(() -> {
+                assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+                assertEquals(FRAME_RATE_CATEGORY_LOW,
+                        mViewRoot.getLastPreferredFrameRateCategory());
+            });
+        });
+        waitForAfterDraw();
+
+        mActivityRule.runOnUiThread(() -> {
+            renderNodeAnimator[0].cancel();
+        });
+
+        // Wait for idle timeout
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                mViewRoot.getLastPreferredFrameRateCategory());
+    }
+
+    @LargeTest
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            com.android.graphics.surfaceflinger.flags.Flags.FLAG_VRR_BUGFIX_24Q4
+    })
+    public void renderNodeAnimatorFrameRateRemoved() throws Throwable {
+        mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+        waitForFrameRateCategoryToSettle();
+
+        RenderNodeAnimator[] renderNodeAnimator = new RenderNodeAnimator[1];
+        renderNodeAnimator[0] = new RenderNodeAnimator(RenderNodeAnimator.ALPHA, 0f);
+        renderNodeAnimator[0].setDuration(100000);
+
+        mActivityRule.runOnUiThread(() -> {
+            renderNodeAnimator[0].setTarget(mMovingView);
+            renderNodeAnimator[0].start();
+            mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            runAfterDraw(() -> {
+                assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+                assertEquals(FRAME_RATE_CATEGORY_LOW,
+                        mViewRoot.getLastPreferredFrameRateCategory());
+            });
+        });
+        waitForAfterDraw();
+
+        mActivityRule.runOnUiThread(() -> {
+            ViewGroup parent = (ViewGroup) mMovingView.getParent();
+            assert parent != null;
+            parent.removeView(mMovingView);
+        });
+
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                mViewRoot.getLastPreferredFrameRateCategory());
+    }
+
     private void runAfterDraw(@NonNull Runnable runnable) {
         Handler handler = new Handler(Looper.getMainLooper());
         mAfterDrawLatch = new CountDownLatch(1);
diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java
index 0650b78..211f74a 100644
--- a/graphics/java/android/graphics/RenderNode.java
+++ b/graphics/java/android/graphics/RenderNode.java
@@ -16,6 +16,7 @@
 
 package android.graphics;
 
+import android.animation.Animator;
 import android.annotation.BytesLong;
 import android.annotation.ColorInt;
 import android.annotation.FloatRange;
@@ -1639,7 +1640,7 @@
      */
     public interface AnimationHost {
         /** @hide */
-        void registerAnimatingRenderNode(RenderNode animator);
+        void registerAnimatingRenderNode(RenderNode renderNode, Animator animator);
 
         /** @hide */
         void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animator);
@@ -1654,7 +1655,7 @@
             throw new IllegalStateException("Cannot start this animator on a detached view!");
         }
         nAddAnimator(mNativeRenderNode, animator.getNativeAnimator());
-        mAnimationHost.registerAnimatingRenderNode(this);
+        mAnimationHost.registerAnimatingRenderNode(this, animator);
     }
 
     /** @hide */
diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
index 55f205b..d4bb461 100644
--- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
@@ -1266,6 +1266,7 @@
         private final IntArray mPendingAnimationActions = new IntArray();
         private final AnimatedVectorDrawable mDrawable;
         private long mTotalDuration;
+        private AnimatorListener mThreadedRendererAnimatorListener;
 
         VectorDrawableAnimatorRT(AnimatedVectorDrawable drawable) {
             mDrawable = drawable;
@@ -1689,6 +1690,9 @@
             if (mListener != null) {
                 mListener.onAnimationStart(null);
             }
+            if (mThreadedRendererAnimatorListener != null) {
+                mThreadedRendererAnimatorListener.onAnimationStart(null);
+            }
         }
 
         // This should only be called after animator has been added to the RenderNode target.
@@ -1717,6 +1721,9 @@
             if (mListener != null) {
                 mListener.onAnimationStart(null);
             }
+            if (mThreadedRendererAnimatorListener != null) {
+                mThreadedRendererAnimatorListener.onAnimationStart(null);
+            }
         }
 
         @Override
@@ -1725,6 +1732,11 @@
         }
 
         @Override
+        public void setThreadedRendererAnimatorListener(AnimatorListener animatorListener) {
+            mThreadedRendererAnimatorListener = animatorListener;
+        }
+
+        @Override
         public boolean canReverse() {
             return mIsReversible;
         }
@@ -1788,6 +1800,9 @@
             if (mListener != null) {
                 mListener.onAnimationEnd(null);
             }
+            if (mThreadedRendererAnimatorListener != null) {
+                mThreadedRendererAnimatorListener.onAnimationEnd(null);
+            }
         }
 
         // onFinished: should be called from native
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 25ba9c5..1df9c88e 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -997,6 +997,13 @@
 }
 
 flag {
+  name: "glanceable_hub_shortcut_button"
+  namespace: "systemui"
+  description: "Shows a button over the dream and lock screen to open the glanceable hub"
+  bug: "339667383"
+}
+
+flag {
   name: "glanceable_hub_gesture_handle"
   namespace: "systemui"
   description: "Shows a vertical bar at the right edge to indicate the user can swipe to open the glanceable hub"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index b124025..978943ae 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -179,8 +179,15 @@
 
     private val fontVariationUtils = FontVariationUtils()
 
-    fun updateLayout(layout: Layout) {
+    fun updateLayout(layout: Layout, textSize: Float = -1f) {
         textInterpolator.layout = layout
+
+        if (textSize >= 0) {
+            textInterpolator.targetPaint.textSize = textSize
+            textInterpolator.basePaint.textSize = textSize
+            textInterpolator.onTargetPaintModified()
+            textInterpolator.onBasePaintModified()
+        }
     }
 
     fun isRunning(): Boolean {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index bdeab79..079c1d9 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -17,7 +17,6 @@
 
 import android.animation.TimeInterpolator
 import android.annotation.ColorInt
-import android.annotation.FloatRange
 import android.annotation.IntRange
 import android.annotation.SuppressLint
 import android.content.Context
@@ -27,7 +26,7 @@
 import android.text.format.DateFormat
 import android.util.AttributeSet
 import android.util.MathUtils.constrainedMap
-import android.util.TypedValue
+import android.util.TypedValue.COMPLEX_UNIT_PX
 import android.view.View
 import android.view.View.MeasureSpec.EXACTLY
 import android.widget.TextView
@@ -219,9 +218,7 @@
 
     override fun setTextSize(type: Int, size: Float) {
         super.setTextSize(type, size)
-        if (type == TypedValue.COMPLEX_UNIT_PX) {
-            lastUnconstrainedTextSize = size
-        }
+        lastUnconstrainedTextSize = if (type == COMPLEX_UNIT_PX) size else Float.MAX_VALUE
     }
 
     @SuppressLint("DrawAllocation")
@@ -234,23 +231,19 @@
                 MeasureSpec.getMode(heightMeasureSpec) == EXACTLY
         ) {
             // Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize
-            super.setTextSize(
-                TypedValue.COMPLEX_UNIT_PX,
-                min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F)
-            )
+            val size = min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F)
+            super.setTextSize(COMPLEX_UNIT_PX, size)
         }
 
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-        val animator = textAnimator
-        if (animator == null) {
-            textAnimator =
-                textAnimatorFactory(layout, ::invalidate)?.also {
-                    onTextAnimatorInitialized?.invoke(it)
-                    onTextAnimatorInitialized = null
-                }
-        } else {
-            animator.updateLayout(layout)
-        }
+        textAnimator?.let { animator -> animator.updateLayout(layout, textSize) }
+            ?: run {
+                textAnimator =
+                    textAnimatorFactory(layout, ::invalidate).also {
+                        onTextAnimatorInitialized?.invoke(it)
+                        onTextAnimatorInitialized = null
+                    }
+            }
 
         if (migratedClocks && hasCustomPositionUpdatedAnimation) {
             // Expand width to avoid clock being clipped during stepping animation
@@ -307,18 +300,18 @@
         logger.d("animateColorChange")
         setTextStyle(
             weight = lockScreenWeight,
-            textSize = -1f,
             color = null, /* using current color */
             animate = false,
+            interpolator = null,
             duration = 0,
             delay = 0,
             onAnimationEnd = null
         )
         setTextStyle(
             weight = lockScreenWeight,
-            textSize = -1f,
             color = lockScreenColor,
             animate = true,
+            interpolator = null,
             duration = COLOR_ANIM_DURATION,
             delay = 0,
             onAnimationEnd = null
@@ -329,16 +322,15 @@
         logger.d("animateAppearOnLockscreen")
         setTextStyle(
             weight = dozingWeight,
-            textSize = -1f,
             color = lockScreenColor,
             animate = false,
+            interpolator = null,
             duration = 0,
             delay = 0,
             onAnimationEnd = null
         )
         setTextStyle(
             weight = lockScreenWeight,
-            textSize = -1f,
             color = lockScreenColor,
             animate = true,
             duration = APPEAR_ANIM_DURATION,
@@ -356,16 +348,15 @@
         logger.d("animateFoldAppear")
         setTextStyle(
             weight = lockScreenWeightInternal,
-            textSize = -1f,
             color = lockScreenColor,
             animate = false,
+            interpolator = null,
             duration = 0,
             delay = 0,
             onAnimationEnd = null
         )
         setTextStyle(
             weight = dozingWeightInternal,
-            textSize = -1f,
             color = dozingColor,
             animate = animate,
             interpolator = Interpolators.EMPHASIZED_DECELERATE,
@@ -385,9 +376,9 @@
         val startAnimPhase2 = Runnable {
             setTextStyle(
                 weight = if (isDozing()) dozingWeight else lockScreenWeight,
-                textSize = -1f,
                 color = null,
                 animate = true,
+                interpolator = null,
                 duration = CHARGE_ANIM_DURATION_PHASE_1,
                 delay = 0,
                 onAnimationEnd = null
@@ -395,9 +386,9 @@
         }
         setTextStyle(
             weight = if (isDozing()) lockScreenWeight else dozingWeight,
-            textSize = -1f,
             color = null,
             animate = true,
+            interpolator = null,
             duration = CHARGE_ANIM_DURATION_PHASE_0,
             delay = chargeAnimationDelay.toLong(),
             onAnimationEnd = startAnimPhase2
@@ -408,9 +399,9 @@
         logger.d("animateDoze")
         setTextStyle(
             weight = if (isDozing) dozingWeight else lockScreenWeight,
-            textSize = -1f,
             color = if (isDozing) dozingColor else lockScreenColor,
             animate = animate,
+            interpolator = null,
             duration = DOZE_ANIM_DURATION,
             delay = 0,
             onAnimationEnd = null
@@ -448,7 +439,6 @@
      */
     private fun setTextStyle(
         @IntRange(from = 0, to = 1000) weight: Int,
-        @FloatRange(from = 0.0) textSize: Float,
         color: Int?,
         animate: Boolean,
         interpolator: TimeInterpolator?,
@@ -459,7 +449,6 @@
         textAnimator?.let {
             it.setTextStyle(
                 weight = weight,
-                textSize = textSize,
                 color = color,
                 animate = animate && isAnimationEnabled,
                 duration = duration,
@@ -474,7 +463,6 @@
                 onTextAnimatorInitialized = { textAnimator ->
                     textAnimator.setTextStyle(
                         weight = weight,
-                        textSize = textSize,
                         color = color,
                         animate = false,
                         duration = duration,
@@ -487,27 +475,6 @@
             }
     }
 
-    private fun setTextStyle(
-        @IntRange(from = 0, to = 1000) weight: Int,
-        @FloatRange(from = 0.0) textSize: Float,
-        color: Int?,
-        animate: Boolean,
-        duration: Long,
-        delay: Long,
-        onAnimationEnd: Runnable?
-    ) {
-        setTextStyle(
-            weight = weight,
-            textSize = textSize,
-            color = color,
-            animate = animate,
-            interpolator = null,
-            duration = duration,
-            delay = delay,
-            onAnimationEnd = onAnimationEnd
-        )
-    }
-
     fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
     fun refreshFormat(use24HourFormat: Boolean) {
         Patterns.update(context)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
index 29fbee0..7936ccc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
@@ -108,7 +108,7 @@
         mTouchHandler.onSessionStart(mTouchSession);
         verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
         inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
-        verify(mCentralSurfaces).handleDreamTouch(motionEvent);
+        verify(mCentralSurfaces).handleCommunalHubTouch(motionEvent);
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
index 26fcb23..49d0399 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
@@ -90,6 +90,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = FakeUserTracker(),
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         settings = FakeSettings()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
index 99a0185..9ab1ac1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
@@ -22,13 +22,14 @@
 import android.content.pm.UserInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.backup.BackupHelper
+import com.android.systemui.res.R
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -80,6 +81,7 @@
                 context = context,
                 userFileManager = userFileManager,
                 userTracker = userTracker,
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
index 567e0a9..159ce36 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -21,7 +21,6 @@
 import android.os.UserHandle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
@@ -32,6 +31,7 @@
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
 import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
+import com.android.systemui.res.R
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.shared.customization.data.content.FakeCustomizationProviderClient
@@ -91,6 +91,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         client1 = FakeCustomizationProviderClient()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 2d77f4f..78a1167 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -148,6 +148,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
similarity index 78%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
index 2b8a644..9dc930b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -27,18 +27,22 @@
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.data.repository.sceneContainerRepository
+import com.android.systemui.scene.data.repository.setSceneTransition
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import junit.framework.Assert.assertEquals
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
@@ -192,6 +196,175 @@
         }
 
     @Test
+    @EnableSceneContainer
+    fun surfaceBehindVisibility_fromLockscreenToGone_trueThroughout() =
+        testScope.runTest {
+            val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+
+            // Before the transition, we start on Lockscreen so the surface should start invisible.
+            kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Lockscreen))
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(isSurfaceBehindVisible).isFalse()
+
+            // Unlocked with fingerprint.
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+
+            // Start the transition to Gone, the surface should become immediately visible.
+            kosmos.setSceneTransition(
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Lockscreen,
+                    toScene = Scenes.Gone,
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                    progress = flowOf(0.3f),
+                    currentScene = flowOf(Scenes.Lockscreen),
+                )
+            )
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(isSurfaceBehindVisible).isTrue()
+
+            // Towards the end of the transition, the surface should continue to be visible.
+            kosmos.setSceneTransition(
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Lockscreen,
+                    toScene = Scenes.Gone,
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                    progress = flowOf(0.9f),
+                    currentScene = flowOf(Scenes.Gone),
+                )
+            )
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(isSurfaceBehindVisible).isTrue()
+
+            // After the transition, settles on Gone. Surface behind should stay visible now.
+            kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Gone))
+            kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            assertThat(isSurfaceBehindVisible).isTrue()
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun surfaceBehindVisibility_fromBouncerToGone_becomesTrue() =
+        testScope.runTest {
+            val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+
+            // Before the transition, we start on Bouncer so the surface should start invisible.
+            kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Bouncer))
+            kosmos.sceneInteractor.changeScene(Scenes.Bouncer, "")
+            assertThat(currentScene).isEqualTo(Scenes.Bouncer)
+            assertThat(isSurfaceBehindVisible).isFalse()
+
+            // Unlocked with fingerprint.
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+
+            // Start the transition to Gone, the surface should remain invisible prior to hitting
+            // the
+            // threshold.
+            kosmos.setSceneTransition(
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Bouncer,
+                    toScene = Scenes.Gone,
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                    progress =
+                        flowOf(
+                            FromPrimaryBouncerTransitionInteractor
+                                .TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD
+                        ),
+                    currentScene = flowOf(Scenes.Bouncer),
+                )
+            )
+            assertThat(currentScene).isEqualTo(Scenes.Bouncer)
+            assertThat(isSurfaceBehindVisible).isFalse()
+
+            // Once the transition passes the threshold, the surface should become visible.
+            kosmos.setSceneTransition(
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Bouncer,
+                    toScene = Scenes.Gone,
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                    progress =
+                        flowOf(
+                            FromPrimaryBouncerTransitionInteractor
+                                .TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD + 0.01f
+                        ),
+                    currentScene = flowOf(Scenes.Gone),
+                )
+            )
+            assertThat(currentScene).isEqualTo(Scenes.Bouncer)
+            assertThat(isSurfaceBehindVisible).isTrue()
+
+            // After the transition, settles on Gone. Surface behind should stay visible now.
+            kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Gone))
+            kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            assertThat(isSurfaceBehindVisible).isTrue()
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun surfaceBehindVisibility_idleWhileUnlocked_alwaysTrue() =
+        testScope.runTest {
+            val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+
+            // Unlocked with fingerprint.
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Gone))
+            kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+
+            listOf(
+                    Scenes.Shade,
+                    Scenes.QuickSettings,
+                    Scenes.Shade,
+                    Scenes.Gone,
+                )
+                .forEach { scene ->
+                    kosmos.setSceneTransition(ObservableTransitionState.Idle(scene))
+                    kosmos.sceneInteractor.changeScene(scene, "")
+                    assertThat(currentScene).isEqualTo(scene)
+                    assertWithMessage("Unexpected visibility for scene \"${scene.debugName}\"")
+                        .that(isSurfaceBehindVisible)
+                        .isTrue()
+                }
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun surfaceBehindVisibility_idleWhileLocked_alwaysFalse() =
+        testScope.runTest {
+            val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+            listOf(
+                    Scenes.Shade,
+                    Scenes.QuickSettings,
+                    Scenes.Shade,
+                    Scenes.Lockscreen,
+                )
+                .forEach { scene ->
+                    kosmos.setSceneTransition(ObservableTransitionState.Idle(scene))
+                    kosmos.sceneInteractor.changeScene(scene, "")
+                    assertWithMessage("Unexpected visibility for scene \"${scene.debugName}\"")
+                        .that(isSurfaceBehindVisible)
+                        .isFalse()
+                }
+        }
+
+    @Test
     @DisableSceneContainer
     fun testUsingGoingAwayAnimation_duringTransitionToGone() =
         testScope.runTest {
diff --git a/packages/SystemUI/res/drawable/ic_widgets.xml b/packages/SystemUI/res/drawable/ic_widgets.xml
new file mode 100644
index 0000000..b21d047
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_widgets.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ 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.
+  -->
+
+<!-- go/gm2-icons, from gs_widgets_vd_theme_24.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M666,520L440,294L666,68L892,294L666,520ZM120,440L120,120L440,120L440,440L120,440ZM520,840L520,520L840,520L840,840L520,840ZM120,840L120,520L440,520L440,840L120,840ZM200,360L360,360L360,200L200,200L200,360ZM667,408L780,295L667,182L554,295L667,408ZM600,760L760,760L760,600L600,600L600,760ZM200,760L360,760L360,600L200,600L200,760ZM360,360L360,360L360,360L360,360L360,360ZM554,295L554,295L554,295L554,295L554,295ZM360,600L360,600L360,600L360,600L360,600ZM600,600L600,600L600,600L600,600L600,600Z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/dream_overlay_open_hub_chip.xml b/packages/SystemUI/res/layout/dream_overlay_open_hub_chip.xml
new file mode 100644
index 0000000..be063a9
--- /dev/null
+++ b/packages/SystemUI/res/layout/dream_overlay_open_hub_chip.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+-->
+<com.android.systemui.animation.view.LaunchableImageView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="@dimen/dream_overlay_bottom_affordance_height"
+    android:layout_width="@dimen/dream_overlay_bottom_affordance_width"
+    android:layout_gravity="bottom|start"
+    android:padding="@dimen/dream_overlay_bottom_affordance_padding"
+    android:scaleType="fitCenter"
+    android:tint="?android:attr/textColorPrimary"
+    android:src="@drawable/ic_widgets"
+    android:contentDescription="@string/accessibility_action_open_communal_hub" />
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 3f3bb0b..86c807b 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -15,12 +15,12 @@
  */
 package com.android.keyguard
 
-import android.os.Trace
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
 import android.content.res.Resources
+import android.os.Trace
 import android.text.format.DateFormat
 import android.util.Log
 import android.util.TypedValue
@@ -466,15 +466,11 @@
         largeRegionSampler?.stopRegionSampler()
         smallTimeListener?.stop()
         largeTimeListener?.stop()
-        clock
-            ?.smallClock
-            ?.view
-            ?.removeOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
+        clock?.apply {
+            smallClock.view.removeOnAttachStateChangeListener(smallClockOnAttachStateChangeListener)
+            largeClock.view.removeOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
+        }
         smallClockFrame?.viewTreeObserver?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
-        clock
-            ?.largeClock
-            ?.view
-            ?.removeOnAttachStateChangeListener(largeClockOnAttachStateChangeListener)
     }
 
     /**
@@ -505,15 +501,17 @@
         }
     }
 
-    private fun updateFontSizes() {
+    fun updateFontSizes() {
         clock?.run {
-            smallClock.events.onFontSettingChanged(
-                resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
-            )
+            smallClock.events.onFontSettingChanged(getSmallClockSizePx())
             largeClock.events.onFontSettingChanged(getLargeClockSizePx())
         }
     }
 
+    private fun getSmallClockSizePx(): Float {
+        return resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
+    }
+
     private fun getLargeClockSizePx(): Float {
         return if (largeClockOnSecondaryDisplay) {
             resources.getDimensionPixelSize(R.dimen.presentation_clock_text_size).toFloat()
@@ -549,9 +547,8 @@
                         it.copy(value = 1f - it.value)
                     },
                     keyguardTransitionInteractor.transition(Edge.create(LOCKSCREEN, AOD)),
-                ).filter {
-                    it.transitionState != TransitionState.FINISHED
-                }
+                )
+                .filter { it.transitionState != TransitionState.FINISHED }
                 .collect { handleDoze(it.value) }
         }
     }
@@ -574,28 +571,27 @@
     internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job {
         return scope.launch {
             keyguardTransitionInteractor
-                    .transitionStepsToState(LOCKSCREEN)
-                    .filter { it.transitionState == TransitionState.STARTED }
-                    .filter { it.from != AOD }
-                    .collect { handleDoze(0f) }
+                .transitionStepsToState(LOCKSCREEN)
+                .filter { it.transitionState == TransitionState.STARTED }
+                .filter { it.from != AOD }
+                .collect { handleDoze(0f) }
         }
     }
 
     /**
-     * When keyguard is displayed due to pulsing notifications when AOD is off,
-     * we should make sure clock is in dozing state instead of LS state
+     * When keyguard is displayed due to pulsing notifications when AOD is off, we should make sure
+     * clock is in dozing state instead of LS state
      */
     @VisibleForTesting
     internal fun listenForAnyStateToDozingTransition(scope: CoroutineScope): Job {
         return scope.launch {
             keyguardTransitionInteractor
-                    .transitionStepsToState(DOZING)
-                    .filter { it.transitionState == TransitionState.FINISHED }
-                    .collect { handleDoze(1f) }
+                .transitionStepsToState(DOZING)
+                .filter { it.transitionState == TransitionState.FINISHED }
+                .collect { handleDoze(1f) }
         }
     }
 
-
     @VisibleForTesting
     internal fun listenForDozing(scope: CoroutineScope): Job {
         return scope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
index afa2375..e284bc7 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
@@ -18,6 +18,7 @@
 
 import static com.android.systemui.complication.dagger.DreamHomeControlsComplicationComponent.DreamHomeControlsModule.DREAM_HOME_CONTROLS_CHIP_VIEW;
 import static com.android.systemui.complication.dagger.RegisteredComplicationsModule.DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS;
+import static com.android.systemui.complication.dagger.RegisteredComplicationsModule.OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.UNAVAILABLE;
@@ -35,6 +36,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.settingslib.Utils;
 import com.android.systemui.CoreStartable;
+import com.android.systemui.Flags;
 import com.android.systemui.animation.ActivityTransitionAnimator;
 import com.android.systemui.complication.dagger.DreamHomeControlsComplicationComponent;
 import com.android.systemui.controls.ControlsServiceInfo;
@@ -89,6 +91,7 @@
         private final DreamHomeControlsComplication mComplication;
         private final DreamOverlayStateController mDreamOverlayStateController;
         private final ControlsComponent mControlsComponent;
+        private final boolean mReplacedByOpenHub;
 
         private boolean mOverlayActive = false;
 
@@ -116,11 +119,13 @@
         public Registrant(DreamHomeControlsComplication complication,
                 DreamOverlayStateController dreamOverlayStateController,
                 ControlsComponent controlsComponent,
-                @SystemUser Monitor monitor) {
+                @SystemUser Monitor monitor,
+                @Named(OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS) boolean replacedByOpenHub) {
             super(monitor);
             mComplication = complication;
             mControlsComponent = controlsComponent;
             mDreamOverlayStateController = dreamOverlayStateController;
+            mReplacedByOpenHub = replacedByOpenHub;
         }
 
         @Override
@@ -132,7 +137,9 @@
 
         private void updateHomeControlsComplication() {
             mControlsComponent.getControlsListingController().ifPresent(c -> {
-                if (isHomeControlsAvailable(c.getCurrentServices())) {
+                final boolean replacedWithOpenHub =
+                        Flags.glanceableHubShortcutButton() && mReplacedByOpenHub;
+                if (isHomeControlsAvailable(c.getCurrentServices()) && !replacedWithOpenHub) {
                     mDreamOverlayStateController.addComplication(mComplication);
                 } else {
                     mDreamOverlayStateController.removeComplication(mComplication);
diff --git a/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java b/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java
new file mode 100644
index 0000000..3cf22b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java
@@ -0,0 +1,212 @@
+/*
+ * 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.complication;
+
+import static com.android.systemui.complication.dagger.OpenHubComplicationComponent.OpenHubModule.OPEN_HUB_CHIP_VIEW;
+import static com.android.systemui.complication.dagger.RegisteredComplicationsModule.OPEN_HUB_CHIP_LAYOUT_PARAMS;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.CoreStartable;
+import com.android.systemui.Flags;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.shared.model.CommunalScenes;
+import com.android.systemui.complication.dagger.OpenHubComplicationComponent;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dagger.qualifiers.SystemUser;
+import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.util.ViewController;
+import com.android.systemui.util.condition.ConditionalCoreStartable;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * A dream complication that shows a chip to open the glanceable hub.
+ */
+// TODO(b/339667383): delete or properly implement this once a product decision is made
+public class OpenHubComplication implements Complication {
+    private final Resources mResources;
+    private final OpenHubComplicationComponent.Factory mComponentFactory;
+
+    @Inject
+    public OpenHubComplication(
+            @Main Resources resources,
+            OpenHubComplicationComponent.Factory componentFactory) {
+        mResources = resources;
+        mComponentFactory = componentFactory;
+    }
+
+    @Override
+    public ViewHolder createView(ComplicationViewModel model) {
+        return mComponentFactory.create(mResources).getViewHolder();
+    }
+
+    @Override
+    public int getRequiredTypeAvailability() {
+        // TODO(b/339667383): create a new complication type if we decide to productionize this
+        return COMPLICATION_TYPE_HOME_CONTROLS;
+    }
+
+    /**
+     * {@link CoreStartable} for registering the complication with SystemUI on startup.
+     */
+    public static class Registrant extends ConditionalCoreStartable {
+        private final OpenHubComplication mComplication;
+        private final DreamOverlayStateController mDreamOverlayStateController;
+
+        private boolean mOverlayActive = false;
+
+        private final DreamOverlayStateController.Callback mOverlayStateCallback =
+                new DreamOverlayStateController.Callback() {
+                    @Override
+                    public void onStateChanged() {
+                        if (mOverlayActive == mDreamOverlayStateController.isOverlayActive()) {
+                            return;
+                        }
+
+                        mOverlayActive = !mOverlayActive;
+
+                        if (mOverlayActive) {
+                            updateOpenHubComplication();
+                        }
+                    }
+                };
+
+        @Inject
+        public Registrant(OpenHubComplication complication,
+                DreamOverlayStateController dreamOverlayStateController,
+                @SystemUser Monitor monitor) {
+            super(monitor);
+            mComplication = complication;
+            mDreamOverlayStateController = dreamOverlayStateController;
+        }
+
+        @Override
+        public void onStart() {
+            mDreamOverlayStateController.addCallback(mOverlayStateCallback);
+        }
+
+        private void updateOpenHubComplication() {
+            // TODO(b/339667383): don't show the complication if glanceable hub is disabled
+            if (Flags.glanceableHubShortcutButton()) {
+                mDreamOverlayStateController.addComplication(mComplication);
+            } else {
+                mDreamOverlayStateController.removeComplication(mComplication);
+            }
+        }
+    }
+
+    /**
+     * Contains values/logic associated with the dream complication view.
+     */
+    public static class OpenHubChipViewHolder implements ViewHolder {
+        private final ImageView mView;
+        private final ComplicationLayoutParams mLayoutParams;
+        private final OpenHubChipViewController mViewController;
+
+        @Inject
+        OpenHubChipViewHolder(
+                OpenHubChipViewController dreamOpenHubChipViewController,
+                @Named(OPEN_HUB_CHIP_VIEW) ImageView view,
+                @Named(OPEN_HUB_CHIP_LAYOUT_PARAMS) ComplicationLayoutParams layoutParams
+        ) {
+            mView = view;
+            mLayoutParams = layoutParams;
+            mViewController = dreamOpenHubChipViewController;
+            mViewController.init();
+        }
+
+        @Override
+        public ImageView getView() {
+            return mView;
+        }
+
+        @Override
+        public ComplicationLayoutParams getLayoutParams() {
+            return mLayoutParams;
+        }
+    }
+
+    /**
+     * Controls behavior of the dream complication.
+     */
+    static class OpenHubChipViewController extends ViewController<ImageView> {
+        private static final String TAG = "OpenHubCtrl";
+        private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+        private final Context mContext;
+        private final ConfigurationController mConfigurationController;
+
+        private final ConfigurationController.ConfigurationListener mConfigurationListener =
+                new ConfigurationController.ConfigurationListener() {
+                    @Override
+                    public void onUiModeChanged() {
+                        reloadResources();
+                    }
+                };
+        private final CommunalInteractor mCommunalInteractor;
+
+        @Inject
+        OpenHubChipViewController(
+                @Named(OPEN_HUB_CHIP_VIEW) ImageView view,
+                Context context,
+                ConfigurationController configurationController,
+                CommunalInteractor communalInteractor) {
+            super(view);
+
+            mContext = context;
+            mConfigurationController = configurationController;
+            mCommunalInteractor = communalInteractor;
+        }
+
+        @Override
+        protected void onViewAttached() {
+            reloadResources();
+            mView.setOnClickListener(this::onClickOpenHub);
+            mConfigurationController.addCallback(mConfigurationListener);
+        }
+
+        @Override
+        protected void onViewDetached() {
+            mConfigurationController.removeCallback(mConfigurationListener);
+        }
+
+        private void reloadResources() {
+            mView.setImageTintList(Utils.getColorAttr(mContext, android.R.attr.textColorPrimary));
+            final Drawable background = mView.getBackground();
+            if (background != null) {
+                background.setTintList(
+                        Utils.getColorAttr(mContext, com.android.internal.R.attr.colorSurface));
+            }
+        }
+
+        private void onClickOpenHub(View v) {
+            if (DEBUG) Log.d(TAG, "open hub complication tapped");
+
+            mCommunalInteractor.changeScene(CommunalScenes.Communal, null);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/complication/dagger/OpenHubComplicationComponent.java b/packages/SystemUI/src/com/android/systemui/complication/dagger/OpenHubComplicationComponent.java
new file mode 100644
index 0000000..501601e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/complication/dagger/OpenHubComplicationComponent.java
@@ -0,0 +1,138 @@
+/*
+ * 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.complication.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+
+import com.android.systemui.complication.OpenHubComplication;
+import com.android.systemui.res.R;
+import com.android.systemui.shared.shadow.DoubleShadowIconDrawable;
+import com.android.systemui.shared.shadow.DoubleShadowTextHelper;
+
+import dagger.BindsInstance;
+import dagger.Module;
+import dagger.Provides;
+import dagger.Subcomponent;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Named;
+import javax.inject.Scope;
+
+/**
+ * Responsible for generating dependencies for the {@link OpenHubComplication}.
+ */
+@Subcomponent(modules = OpenHubComplicationComponent.OpenHubModule.class)
+@OpenHubComplicationComponent.OpenHubComplicationScope
+public interface OpenHubComplicationComponent {
+    /**
+     * Creates a view holder for the open hub complication.
+     */
+    OpenHubComplication.OpenHubChipViewHolder getViewHolder();
+
+    /**
+     * Scope of the open hub complication.
+     */
+    @Documented
+    @Retention(RUNTIME)
+    @Scope
+    @interface OpenHubComplicationScope {
+    }
+
+    /**
+     * Factory that generates a {@link OpenHubComplicationComponent}.
+     */
+    @Subcomponent.Factory
+    interface Factory {
+        /**
+         * Creates an instance of {@link OpenHubComplicationComponent}.
+         */
+        OpenHubComplicationComponent create(@BindsInstance Resources resources);
+    }
+
+    /**
+     * Scoped injected values for the {@link OpenHubComplicationComponent}.
+     */
+    @Module
+    interface OpenHubModule {
+        String OPEN_HUB_CHIP_VIEW = "open_hub_chip_view";
+        String OPEN_HUB_BACKGROUND_DRAWABLE = "open_hub_background_drawable";
+
+        /**
+         * Provides the dream open hub chip view.
+         */
+        @Provides
+        @OpenHubComplicationScope
+        @Named(OPEN_HUB_CHIP_VIEW)
+        static ImageView provideOpenHubChipView(
+                LayoutInflater layoutInflater,
+                @Named(OPEN_HUB_BACKGROUND_DRAWABLE) Drawable backgroundDrawable) {
+            final ImageView chip =
+                    (ImageView) layoutInflater.inflate(R.layout.dream_overlay_open_hub_chip,
+                            null, false);
+            chip.setBackground(backgroundDrawable);
+
+            return chip;
+        }
+
+        /**
+         * Provides the background drawable for the open hub chip.
+         */
+        @Provides
+        @OpenHubComplicationScope
+        @Named(OPEN_HUB_BACKGROUND_DRAWABLE)
+        static Drawable providesOpenHubBackground(Context context, Resources resources) {
+            return new DoubleShadowIconDrawable(createShadowInfo(
+                    resources,
+                    R.dimen.dream_overlay_bottom_affordance_key_text_shadow_radius,
+                    R.dimen.dream_overlay_bottom_affordance_key_text_shadow_dx,
+                    R.dimen.dream_overlay_bottom_affordance_key_text_shadow_dy,
+                    R.dimen.dream_overlay_bottom_affordance_key_shadow_alpha
+            ),
+                    createShadowInfo(
+                            resources,
+                            R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_radius,
+                            R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_dx,
+                            R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_dy,
+                            R.dimen.dream_overlay_bottom_affordance_ambient_shadow_alpha
+                    ),
+                    resources.getDrawable(R.drawable.dream_overlay_bottom_affordance_bg),
+                    resources.getDimensionPixelOffset(
+                            R.dimen.dream_overlay_bottom_affordance_width),
+                    resources.getDimensionPixelSize(R.dimen.dream_overlay_bottom_affordance_inset)
+            );
+        }
+
+        private static DoubleShadowTextHelper.ShadowInfo createShadowInfo(Resources resources,
+                int blurId, int offsetXId, int offsetYId, int alphaId) {
+
+            return new DoubleShadowTextHelper.ShadowInfo(
+                    resources.getDimension(blurId),
+                    resources.getDimension(offsetXId),
+                    resources.getDimension(offsetYId),
+                    resources.getFloat(alphaId)
+            );
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/complication/dagger/RegisteredComplicationsModule.java
index 6f1b098..edb5ff7 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/dagger/RegisteredComplicationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/dagger/RegisteredComplicationsModule.java
@@ -25,6 +25,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.res.R;
+import com.android.systemui.util.settings.SystemSettings;
 
 import dagger.Module;
 import dagger.Provides;
@@ -39,6 +40,7 @@
         subcomponents = {
                 DreamClockTimeComplicationComponent.class,
                 DreamHomeControlsComplicationComponent.class,
+                OpenHubComplicationComponent.class,
                 DreamMediaEntryComplicationComponent.class
         })
 public interface RegisteredComplicationsModule {
@@ -46,6 +48,8 @@
     String DREAM_SMARTSPACE_LAYOUT_PARAMS = "smartspace_layout_params";
     String DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS = "home_controls_chip_layout_params";
     String DREAM_MEDIA_ENTRY_LAYOUT_PARAMS = "media_entry_layout_params";
+    String OPEN_HUB_CHIP_LAYOUT_PARAMS = "open_hub_chip_layout_params";
+    String OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS = "open_hub_chip_replace_home_controls";
 
     int DREAM_CLOCK_TIME_COMPLICATION_WEIGHT = 1;
     int DREAM_CLOCK_TIME_COMPLICATION_WEIGHT_NO_SMARTSPACE = 2;
@@ -109,6 +113,26 @@
     }
 
     /**
+     * Provides layout parameters for the open hub complication.
+     */
+    @Provides
+    @Named(OPEN_HUB_CHIP_LAYOUT_PARAMS)
+    static ComplicationLayoutParams provideOpenHubLayoutParams(
+            @Named(OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS) boolean replaceHomeControls) {
+        int position = ComplicationLayoutParams.POSITION_BOTTOM | (replaceHomeControls
+                ? ComplicationLayoutParams.POSITION_START
+                : ComplicationLayoutParams.POSITION_END);
+        int direction = replaceHomeControls ? ComplicationLayoutParams.DIRECTION_END
+                : ComplicationLayoutParams.DIRECTION_START;
+        return new ComplicationLayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                position,
+                direction,
+                DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT);
+    }
+
+    /**
      * Provides layout parameters for the smartspace complication.
      */
     @Provides
@@ -124,4 +148,14 @@
                 res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_padding),
                 res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_max_width));
     }
+
+    /**
+     * If true, the home controls chip should not be shown and the open hub chip should be shown in
+     * its place.
+     */
+    @Provides
+    @Named(OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS)
+    static boolean providesOpenHubChipReplaceHomeControls(SystemSettings systemSettings) {
+        return systemSettings.getBool(OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS, false);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 3e98fc1..7aab37e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -32,6 +32,8 @@
 import com.android.systemui.dock.DockManagerImpl;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule;
+import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule;
+import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule;
 import com.android.systemui.media.dagger.MediaModule;
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
@@ -112,6 +114,8 @@
         GestureModule.class,
         HeadsUpModule.class,
         KeyboardShortcutsModule.class,
+        KeyguardBlueprintModule.class,
+        KeyguardSectionsModule.class,
         MediaModule.class,
         MediaMuteAwaitConnectionCli.StartableModule.class,
         MultiUserUtilsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 339e8f0..2ebb94f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -70,8 +70,6 @@
 import com.android.systemui.keyboard.KeyboardModule;
 import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
 import com.android.systemui.keyguard.ui.composable.LockscreenContent;
-import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule;
-import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule;
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.log.dagger.MonitorLog;
 import com.android.systemui.log.table.TableLogBuffer;
@@ -222,8 +220,6 @@
         InputMethodModule.class,
         KeyEventRepositoryModule.class,
         KeyboardModule.class,
-        KeyguardBlueprintModule.class,
-        KeyguardSectionsModule.class,
         LetterboxModule.class,
         LogModule.class,
         MediaProjectionActivitiesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
index 1c047dd..04fda33 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -98,7 +98,7 @@
         // Notification shade window has its own logic to be visible if the hub is open, no need to
         // do anything here other than send touch events over.
         session.registerInputListener(ev -> {
-            surfaces.handleDreamTouch((MotionEvent) ev);
+            surfaces.handleCommunalHubTouch((MotionEvent) ev);
             if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
                 var unused = session.pop();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 2e49919..84980c1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -170,12 +170,6 @@
     val WALLPAPER_PICKER_GRID_APPLY_BUTTON =
             unreleasedFlag("wallpaper_picker_grid_apply_button")
 
-    /** Keyguard Migration */
-
-    // TODO(b/297037052): Tracking bug.
-    @JvmField
-    val REMOVE_NPVC_BOTTOM_AREA_USAGE = unreleasedFlag("remove_npvc_bottom_area_usage")
-
     /** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */
     // TODO(b/286563884): Tracking bug
     @JvmField val KEYGUARD_TALKBACK_FIX = unreleasedFlag("keyguard_talkback_fix")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index c32c226..a50cc8f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -53,7 +53,6 @@
 import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
 import com.android.systemui.keyguard.ui.view.KeyguardRootView
-import com.android.systemui.keyguard.ui.view.layout.KeyguardBlueprintCommandListener
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
@@ -89,7 +88,6 @@
     private val screenOffAnimationController: ScreenOffAnimationController,
     private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
     private val chipbarCoordinator: ChipbarCoordinator,
-    private val keyguardBlueprintCommandListener: KeyguardBlueprintCommandListener,
     private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
     private val keyguardStatusViewComponentFactory: KeyguardStatusViewComponent.Factory,
     private val configuration: ConfigurationState,
@@ -160,7 +158,6 @@
                 )
             }
         }
-        keyguardBlueprintCommandListener.start()
     }
 
     fun bindIndicationArea() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
index 80675d3..0863cd7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -28,6 +28,8 @@
     const val CREATE_NOTE = "create_note"
     const val DO_NOT_DISTURB = "do_not_disturb"
     const val FLASHLIGHT = "flashlight"
+    // TODO(b/339667383): delete or properly implement this once a product decision is made
+    const val GLANCEABLE_HUB = "glanceable_hub"
     const val HOME_CONTROLS = "home"
     const val MUTE = "mute"
     const val QR_CODE_SCANNER = "qr_code_scanner"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt
new file mode 100644
index 0000000..d09b9f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import com.android.systemui.Flags
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.communal.data.repository.CommunalSceneRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/** Shortcut that opens the glanceable hub. */
+// TODO(b/339667383): delete or properly implement this once a product decision is made
+@SysUISingleton
+class GlanceableHubQuickAffordanceConfig
+@Inject
+constructor(
+    private val communalRepository: CommunalSceneRepository,
+) : KeyguardQuickAffordanceConfig {
+
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.GLANCEABLE_HUB
+    override fun pickerName(): String = "Glanceable hub"
+
+    override val pickerIconResourceId = R.drawable.ic_widgets
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> by lazy {
+        if (Flags.glanceableHubShortcutButton()) {
+            val contentDescription = ContentDescription.Loaded(pickerName())
+            val icon = Icon.Resource(pickerIconResourceId, contentDescription)
+            flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon))
+        } else {
+            flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+        }
+    }
+
+    override fun onTriggered(
+        expandable: Expandable?
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        communalRepository.changeScene(CommunalScenes.Communal, null)
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index 4556195..93296f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -36,6 +36,7 @@
             camera: CameraQuickAffordanceConfig,
             doNotDisturb: DoNotDisturbQuickAffordanceConfig,
             flashlight: FlashlightQuickAffordanceConfig,
+            glanceableHub: GlanceableHubQuickAffordanceConfig,
             home: HomeControlsKeyguardQuickAffordanceConfig,
             mute: MuteQuickAffordanceConfig,
             quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
@@ -46,6 +47,7 @@
                 camera,
                 doNotDisturb,
                 flashlight,
+                glanceableHub,
                 home,
                 mute,
                 quickAccessWallet,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
index deedbdb..0748979 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
@@ -20,15 +20,17 @@
 import android.content.Context
 import android.content.IntentFilter
 import android.content.SharedPreferences
-import com.android.systemui.res.R
+import com.android.systemui.Flags
 import com.android.systemui.backup.BackupHelper
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.settings.SystemSettings
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
@@ -50,6 +52,7 @@
     @Application private val context: Context,
     private val userFileManager: UserFileManager,
     private val userTracker: UserTracker,
+    private val systemSettings: SystemSettings,
     broadcastDispatcher: BroadcastDispatcher,
 ) : KeyguardQuickAffordanceSelectionManager {
 
@@ -70,6 +73,22 @@
     }
 
     private val defaults: Map<String, List<String>> by lazy {
+        // Quick hack to allow testing out a lock screen shortcut to open the glanceable hub. This
+        // flag will not be rolled out and is only used for local testing.
+        // TODO(b/339667383): delete or properly implement this once a product decision is made
+        if (Flags.glanceableHubShortcutButton()) {
+            if (systemSettings.getBool("open_hub_chip_replace_home_controls", false)) {
+                return@lazy mapOf(
+                    "bottom_start" to listOf("glanceable_hub"),
+                    "bottom_end" to listOf("create_note")
+                )
+            } else {
+                return@lazy mapOf(
+                    "bottom_start" to listOf("home"),
+                    "bottom_end" to listOf("glanceable_hub")
+                )
+            }
+        }
         context.resources
             .getStringArray(R.array.config_keyguardQuickAffordanceDefaults)
             .associate { item ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index c11c49c..b826a00 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -91,9 +91,9 @@
      */
     fun refreshBlueprint(config: Config = Config.DEFAULT) {
         fun scheduleCallback() {
-            // We use a handler here instead of a CoroutineDipsatcher because the one provided by
+            // We use a handler here instead of a CoroutineDispatcher because the one provided by
             // @Main CoroutineDispatcher is currently Dispatchers.Main.immediate, which doesn't
-            // delay the callback, and instead runs it imemdiately.
+            // delay the callback, and instead runs it immediately.
             handler.post {
                 assert.isMainThread()
                 targetTransitionConfig?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index 53a0c32..76a8223 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -86,7 +86,7 @@
                     return@combine null
                 }
 
-                fromBouncerStep.value > 0.5f
+                fromBouncerStep.value > TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD
             }
             .onStart {
                 // Default to null ("don't care, use a reasonable default").
@@ -232,5 +232,6 @@
         val TO_AOD_DURATION = DEFAULT_DURATION
         val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
         val TO_DOZING_DURATION = DEFAULT_DURATION
+        val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.5f
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index 7cee258..41c3959 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -20,6 +20,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.content.Context
+import com.android.systemui.CoreStartable
 import com.android.systemui.biometrics.domain.interactor.FingerprintPropertyInteractor
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
@@ -31,17 +32,17 @@
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
+import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
+import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -53,9 +54,11 @@
     private val context: Context,
     private val shadeInteractor: ShadeInteractor,
     private val clockInteractor: KeyguardClockInteractor,
-    configurationInteractor: ConfigurationInteractor,
-    fingerprintPropertyInteractor: FingerprintPropertyInteractor,
-) {
+    private val configurationInteractor: ConfigurationInteractor,
+    private val fingerprintPropertyInteractor: FingerprintPropertyInteractor,
+    private val smartspaceSection: SmartspaceSection,
+    private val clockSection: ClockSection,
+) : CoreStartable {
     /** The current blueprint for the lockscreen. */
     val blueprint: StateFlow<KeyguardBlueprint> = keyguardBlueprintRepository.blueprint
 
@@ -75,15 +78,23 @@
             }
         }
 
-    private val refreshEvents: Flow<Unit> =
-        merge(
-            configurationInteractor.onAnyConfigurationChange,
-            fingerprintPropertyInteractor.propertiesInitialized.filter { it }.map {},
-        )
-
-    init {
+    override fun start() {
         applicationScope.launch { blueprintId.collect { transitionToBlueprint(it) } }
-        applicationScope.launch { refreshEvents.collect { refreshBlueprint() } }
+        applicationScope.launch {
+            fingerprintPropertyInteractor.propertiesInitialized
+                .filter { it }
+                .collect { refreshBlueprint() }
+        }
+        applicationScope.launch {
+            val refreshConfig =
+                Config(
+                    Type.NoTransition,
+                    rebuildSections = listOf(smartspaceSection),
+                )
+            configurationInteractor.onAnyConfigurationChange.collect {
+                refreshBlueprint(refreshConfig)
+            }
+        }
     }
 
     /**
@@ -120,4 +131,8 @@
     fun getCurrentBlueprint(): KeyguardBlueprint {
         return keyguardBlueprintRepository.blueprint.value
     }
+
+    companion object {
+        private val TAG = "KeyguardBlueprintInteractor"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
index fb65a6d..88e6602 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
@@ -29,6 +30,7 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
 import com.android.systemui.util.kotlin.sample
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -84,25 +86,52 @@
             }
             .distinctUntilChanged()
 
+    private val isDeviceEntered: Flow<Boolean> by lazy {
+        deviceEntryInteractor.get().isDeviceEntered
+    }
+
+    private val isDeviceNotEntered: Flow<Boolean> by lazy { isDeviceEntered.map { !it } }
+
     /**
-     * Surface visibility, which is either determined by the default visibility in the FINISHED
-     * KeyguardState, or the transition-specific visibility used during certain RUNNING transitions.
+     * Surface visibility, which is either determined by the default visibility when not
+     * transitioning between [KeyguardState]s or [Scenes] or the transition-specific visibility used
+     * during certain ongoing transitions.
      */
     @OptIn(ExperimentalCoroutinesApi::class)
     val surfaceBehindVisibility: Flow<Boolean> =
-        transitionInteractor.isInTransitionToAnyState
-            .flatMapLatest { isInTransition ->
-                if (!isInTransition) {
-                    defaultSurfaceBehindVisibility
-                } else {
-                    combine(
-                        transitionSpecificSurfaceBehindVisibility,
-                        defaultSurfaceBehindVisibility,
-                    ) { transitionVisibility, defaultVisibility ->
-                        // Defer to the transition-specific visibility since we're RUNNING a
-                        // transition, but fall back to the default visibility if the current
-                        // transition's interactor did not specify a visibility.
-                        transitionVisibility ?: defaultVisibility
+        if (SceneContainerFlag.isEnabled) {
+                sceneInteractor.get().transitionState.flatMapLatestConflated { transitionState ->
+                    when (transitionState) {
+                        is ObservableTransitionState.Transition ->
+                            when {
+                                transitionState.fromScene == Scenes.Lockscreen &&
+                                    transitionState.toScene == Scenes.Gone -> flowOf(true)
+                                transitionState.fromScene == Scenes.Bouncer &&
+                                    transitionState.toScene == Scenes.Gone ->
+                                    transitionState.progress.map { progress ->
+                                        progress >
+                                            FromPrimaryBouncerTransitionInteractor
+                                                .TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD
+                                    }
+                                else -> isDeviceEntered
+                            }
+                        is ObservableTransitionState.Idle -> isDeviceEntered
+                    }
+                }
+            } else {
+                transitionInteractor.isInTransitionToAnyState.flatMapLatest { isInTransition ->
+                    if (!isInTransition) {
+                        defaultSurfaceBehindVisibility
+                    } else {
+                        combine(
+                            transitionSpecificSurfaceBehindVisibility,
+                            defaultSurfaceBehindVisibility,
+                        ) { transitionVisibility, defaultVisibility ->
+                            // Defer to the transition-specific visibility since we're RUNNING a
+                            // transition, but fall back to the default visibility if the current
+                            // transition's interactor did not specify a visibility.
+                            transitionVisibility ?: defaultVisibility
+                        }
                     }
                 }
             }
@@ -162,7 +191,7 @@
      */
     val lockscreenVisibility: Flow<Boolean> =
         if (SceneContainerFlag.isEnabled) {
-            deviceEntryInteractor.get().isDeviceEntered.map { !it }
+            isDeviceNotEntered
         } else {
             transitionInteractor.currentKeyguardState
                 .sample(transitionInteractor.startedStepWithPrecedingStep, ::Pair)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBlueprint.kt
index 7ca2eba..6d579f3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBlueprint.kt
@@ -37,16 +37,32 @@
     fun replaceViews(
         constraintLayout: ConstraintLayout,
         previousBlueprint: KeyguardBlueprint? = null,
+        rebuildSections: List<KeyguardSection> = listOf(),
         bindData: Boolean = true
     ) {
-        val prevSections =
-            previousBlueprint?.let { prev ->
-                prev.sections.subtract(sections).forEach { it.removeViews(constraintLayout) }
-                prev.sections
+        val prevSections = previousBlueprint?.sections ?: listOf()
+        val skipSections = sections.intersect(prevSections).subtract(rebuildSections)
+        prevSections.subtract(skipSections).forEach { it.removeViews(constraintLayout) }
+        sections.subtract(skipSections).forEach {
+            it.addViews(constraintLayout)
+            if (bindData) {
+                it.bindData(constraintLayout)
             }
-                ?: listOf()
+        }
+    }
 
-        sections.subtract(prevSections).forEach {
+    /** Rebuilds views for the target sections, or all of them if unspecified. */
+    fun rebuildViews(
+        constraintLayout: ConstraintLayout,
+        rebuildSections: List<KeyguardSection> = sections,
+        bindData: Boolean = true
+    ) {
+        if (rebuildSections.isEmpty()) {
+            return
+        }
+
+        rebuildSections.forEach { it.removeViews(constraintLayout) }
+        rebuildSections.forEach {
             it.addViews(constraintLayout)
             if (bindData) {
                 it.bindData(constraintLayout)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 52d7519..8160335 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -95,17 +95,8 @@
                             null as KeyguardBlueprint?,
                         )
                         .collect { (prevBlueprint, blueprint) ->
-                            val cs =
-                                ConstraintSet().apply {
-                                    clone(constraintLayout)
-                                    val emptyLayout = ConstraintSet.Layout()
-                                    knownIds.forEach {
-                                        getConstraint(it).layout.copyFrom(emptyLayout)
-                                    }
-                                    blueprint.applyConstraints(this)
-                                }
-
-                            var transition =
+                            val config = Config.DEFAULT
+                            val transition =
                                 if (
                                     !KeyguardBottomAreaRefactor.isEnabled &&
                                         prevBlueprint != null &&
@@ -114,23 +105,37 @@
                                     BaseBlueprintTransition(clockViewModel)
                                         .addTransition(
                                             IntraBlueprintTransition(
-                                                Config.DEFAULT,
+                                                config,
                                                 clockViewModel,
                                                 smartspaceViewModel
                                             )
                                         )
                                 } else {
                                     IntraBlueprintTransition(
-                                        Config.DEFAULT,
+                                        config,
                                         clockViewModel,
                                         smartspaceViewModel
                                     )
                                 }
 
-                            runTransition(constraintLayout, transition, Config.DEFAULT) {
-                                // Add and remove views of sections that are not contained by the
-                                // other.
-                                blueprint.replaceViews(constraintLayout, prevBlueprint)
+                            runTransition(constraintLayout, transition, config) {
+                                // Replace sections from the previous blueprint with the new ones
+                                blueprint.replaceViews(
+                                    constraintLayout,
+                                    prevBlueprint,
+                                    config.rebuildSections
+                                )
+
+                                val cs =
+                                    ConstraintSet().apply {
+                                        clone(constraintLayout)
+                                        val emptyLayout = ConstraintSet.Layout()
+                                        knownIds.forEach {
+                                            getConstraint(it).layout.copyFrom(emptyLayout)
+                                        }
+                                        blueprint.applyConstraints(this)
+                                    }
+
                                 logAlphaVisibilityOfAppliedConstraintSet(cs, clockViewModel)
                                 cs.applyTo(constraintLayout)
                             }
@@ -138,22 +143,21 @@
                 }
 
                 launch("$TAG#viewModel.refreshTransition") {
-                    viewModel.refreshTransition.collect { transition ->
-                        val cs =
-                            ConstraintSet().apply {
-                                clone(constraintLayout)
-                                viewModel.blueprint.value.applyConstraints(this)
-                            }
+                    viewModel.refreshTransition.collect { config ->
+                        val blueprint = viewModel.blueprint.value
 
                         runTransition(
                             constraintLayout,
-                            IntraBlueprintTransition(
-                                transition,
-                                clockViewModel,
-                                smartspaceViewModel
-                            ),
-                            transition,
+                            IntraBlueprintTransition(config, clockViewModel, smartspaceViewModel),
+                            config,
                         ) {
+                            blueprint.rebuildViews(constraintLayout, config.rebuildSections)
+
+                            val cs =
+                                ConstraintSet().apply {
+                                    clone(constraintLayout)
+                                    blueprint.applyConstraints(this)
+                                }
                             logAlphaVisibilityOfAppliedConstraintSet(cs, clockViewModel)
                             cs.applyTo(constraintLayout)
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
index 962cdf1..c026656 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.ui.view.layout
 
 import androidx.core.text.isDigitsOnly
+import com.android.systemui.CoreStartable
 import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.statusbar.commandline.Command
@@ -31,10 +32,10 @@
     private val commandRegistry: CommandRegistry,
     private val keyguardBlueprintRepository: KeyguardBlueprintRepository,
     private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
-) {
+) : CoreStartable {
     private val layoutCommand = KeyguardLayoutManagerCommand()
 
-    fun start() {
+    override fun start() {
         commandRegistry.registerCommand(COMMAND) { layoutCommand }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
index 04ac7bf..2dc9301 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
@@ -17,9 +17,14 @@
 
 package com.android.systemui.keyguard.ui.view.layout.blueprints
 
+import com.android.systemui.CoreStartable
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
+import com.android.systemui.keyguard.ui.view.layout.KeyguardBlueprintCommandListener
 import dagger.Binds
 import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 import dagger.multibindings.IntoSet
 
 @Module
@@ -41,4 +46,18 @@
     abstract fun bindShortcutsBesideUdfpsLockscreenBlueprint(
         shortcutsBesideUdfpsLockscreenBlueprint: ShortcutsBesideUdfpsKeyguardBlueprint
     ): KeyguardBlueprint
+
+    @Binds
+    @IntoMap
+    @ClassKey(KeyguardBlueprintInteractor::class)
+    abstract fun bindsKeyguardBlueprintInteractor(
+        keyguardBlueprintInteractor: KeyguardBlueprintInteractor
+    ): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(KeyguardBlueprintCommandListener::class)
+    abstract fun bindsKeyguardBlueprintCommandListener(
+        keyguardBlueprintCommandListener: KeyguardBlueprintCommandListener
+    ): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index c69d868..02e9ca5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.ui.view.layout.blueprints.transitions
 
 import android.transition.TransitionSet
+import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition
 import com.android.systemui.keyguard.ui.view.layout.sections.transitions.DefaultClockSteppingTransition
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
@@ -46,6 +47,7 @@
         val type: Type,
         val checkPriority: Boolean = true,
         val terminatePrevious: Boolean = true,
+        val rebuildSections: List<KeyguardSection> = listOf(),
     ) {
         companion object {
             val DEFAULT = Config(Type.NoTransition)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index b367715..34a1da5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -32,6 +32,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.VISIBLE
 import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import com.android.systemui.customization.R as custR
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
@@ -57,6 +58,7 @@
     alpha: Float,
 ) = views.forEach { view -> this.setAlpha(view.id, alpha) }
 
+@SysUISingleton
 class ClockSection
 @Inject
 constructor(
@@ -72,6 +74,7 @@
         if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
+
         KeyguardClockViewBinder.bind(
             this,
             constraintLayout,
@@ -86,6 +89,7 @@
         if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
+
         keyguardClockViewModel.currentClock.value?.let { clock ->
             constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet))
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index 0b8376a..c5fab8f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -219,5 +219,37 @@
                 sensorRect.left
             )
         }
+
+        // This is only intended to be here until the KeyguardBottomAreaRefactor flag is enabled
+        // Without this logic, the lock icon location changes but the KeyguardBottomAreaView is not
+        // updated and visible ui layout jank occurs. This is due to AmbientIndicationContainer
+        // being in NPVC and laying out prior to the KeyguardRootView.
+        // Remove when both DeviceEntryUdfpsRefactor and KeyguardBottomAreaRefactor are enabled.
+        if (DeviceEntryUdfpsRefactor.isEnabled && !KeyguardBottomAreaRefactor.isEnabled) {
+            with(notificationPanelView) {
+                val isUdfpsSupported = deviceEntryIconViewModel.get().isUdfpsSupported.value
+                val bottomAreaViewRight = findViewById<View>(R.id.keyguard_bottom_area)?.right ?: 0
+                findViewById<View>(R.id.ambient_indication_container)?.let {
+                    val (ambientLeft, ambientTop) = it.locationOnScreen
+                    if (isUdfpsSupported) {
+                        // make top of ambient indication view the bottom of the lock icon
+                        it.layout(
+                            ambientLeft,
+                            sensorRect.bottom,
+                            bottomAreaViewRight - ambientLeft,
+                            ambientTop + it.measuredHeight
+                        )
+                    } else {
+                        // make bottom of ambient indication view the top of the lock icon
+                        it.layout(
+                            ambientLeft,
+                            sensorRect.top - it.measuredHeight,
+                            bottomAreaViewRight - ambientLeft,
+                            sensorRect.top
+                        )
+                    }
+                }
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index 487c2e9..2d6690f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -22,6 +22,7 @@
 import androidx.constraintlayout.widget.Barrier
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
@@ -36,6 +37,7 @@
 import dagger.Lazy
 import javax.inject.Inject
 
+@SysUISingleton
 open class SmartspaceSection
 @Inject
 constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 22aa492..1d8b7e5b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.communal.ui.compose.CommunalContent
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.util.CommunalColors
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -64,6 +65,7 @@
  *
  * This will be used until the glanceable hub is integrated into Flexiglass.
  */
+@SysUISingleton
 class GlanceableHubContainerController
 @Inject
 constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 7b7a35b..05a4391 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -327,6 +327,11 @@
     @Deprecated
     float getDisplayDensity();
 
+    /**
+     * Forwards touch events to communal hub
+     */
+    void handleCommunalHubTouch(MotionEvent event);
+
     public static class KeyboardShortcutsMessage {
         final int mDeviceId;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
index 5ab56ae..a7b5484 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
@@ -81,6 +81,7 @@
     override fun shouldIgnoreTouch() = false
     override fun isDeviceInteractive() = false
     override fun handleDreamTouch(event: MotionEvent?) {}
+    override fun handleCommunalHubTouch(event: MotionEvent?) {}
     override fun awakenDreams() {}
     override fun isBouncerShowing() = false
     override fun isBouncerShowingScrimmed() = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index e0da2fe..78a8036 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -172,6 +172,7 @@
 import com.android.systemui.settings.brightness.BrightnessSliderController;
 import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor;
 import com.android.systemui.shade.CameraLauncher;
+import com.android.systemui.shade.GlanceableHubContainerController;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.QuickSettingsController;
@@ -595,6 +596,7 @@
     private final ColorExtractor.OnColorsChangedListener mOnColorsChangedListener =
             (extractor, which) -> updateTheme();
     private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor;
+    private final GlanceableHubContainerController mGlanceableHubContainerController;
 
     /**
      * Public constructor for CentralSurfaces.
@@ -707,7 +709,8 @@
             UserTracker userTracker,
             Provider<FingerprintManager> fingerprintManager,
             ActivityStarter activityStarter,
-            BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor
+            BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor,
+            GlanceableHubContainerController glanceableHubContainerController
     ) {
         mContext = context;
         mNotificationsController = notificationsController;
@@ -802,6 +805,7 @@
         mFingerprintManager = fingerprintManager;
         mActivityStarter = activityStarter;
         mBrightnessMirrorShowingInteractor = brightnessMirrorShowingInteractor;
+        mGlanceableHubContainerController = glanceableHubContainerController;
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mStartingSurfaceOptional = startingSurfaceOptional;
@@ -2951,6 +2955,11 @@
     }
 
     @Override
+    public void handleCommunalHubTouch(MotionEvent event) {
+        mGlanceableHubContainerController.onTouchEvent(event);
+    }
+
+    @Override
     public void awakenDreams() {
         mUiBgExecutor.execute(() -> {
             try {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
index 58b82f1..da928a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
@@ -32,6 +32,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.camera.flags.Flags;
+import com.android.systemui.util.ListenerSet;
 
 import java.util.Set;
 
@@ -43,7 +44,7 @@
     private final SparseBooleanArray mSoftwareToggleState = new SparseBooleanArray();
     private final SparseBooleanArray mHardwareToggleState = new SparseBooleanArray();
     private Boolean mRequiresAuthentication;
-    private final Set<Callback> mCallbacks = new ArraySet<>();
+    private final ListenerSet<Callback> mCallbacks = new ListenerSet<>();
 
     public IndividualSensorPrivacyControllerImpl(
             @NonNull SensorPrivacyManager sensorPrivacyManager) {
@@ -115,7 +116,7 @@
 
     @Override
     public void addCallback(@NonNull Callback listener) {
-        mCallbacks.add(listener);
+        mCallbacks.addIfAbsent(listener);
     }
 
     @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java
index 07c980b..18bd960b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java
@@ -132,7 +132,7 @@
     public void complicationAvailability_serviceNotAvailable_noFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
         registrant.start();
 
         setHaveFavorites(false);
@@ -145,7 +145,7 @@
     public void complicationAvailability_serviceAvailable_noFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
         registrant.start();
 
         setHaveFavorites(false);
@@ -158,7 +158,7 @@
     public void complicationAvailability_serviceAvailable_noFavorites_panel_addComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
         registrant.start();
 
         setHaveFavorites(false);
@@ -171,7 +171,7 @@
     public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
         registrant.start();
 
         setHaveFavorites(true);
@@ -184,7 +184,7 @@
     public void complicationAvailability_serviceAvailable_haveFavorites_addComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
         registrant.start();
 
         setHaveFavorites(true);
@@ -197,7 +197,7 @@
     public void complicationAvailability_checkAvailabilityWhenDreamOverlayBecomesActive() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
         registrant.start();
 
         setServiceAvailable(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index 36bfa09..90ac05f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -135,6 +135,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index 0bdf47a..f0ad510 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -48,6 +49,9 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @ExperimentalCoroutinesApi
@@ -57,6 +61,7 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val underTest = kosmos.keyguardBlueprintInteractor
+    private val keyguardBlueprintRepository = kosmos.keyguardBlueprintRepository
     private val clockRepository by lazy { kosmos.fakeKeyguardClockRepository }
     private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
     private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository }
@@ -103,20 +108,6 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
-    fun fingerprintPropertyInitialized_updatesBlueprint() {
-        testScope.runTest {
-            assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull()
-
-            fingerprintPropertyRepository.supportsUdfps() // initialize properties
-
-            runCurrent()
-            advanceUntilIdle()
-            assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull()
-        }
-    }
-
-    @Test
     @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
     fun testDoesNotApplySplitShadeBlueprint() {
         testScope.runTest {
@@ -132,15 +123,31 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+    fun fingerprintPropertyInitialized_updatesBlueprint() {
+        testScope.runTest {
+            underTest.start()
+            reset(keyguardBlueprintRepository)
+
+            fingerprintPropertyRepository.supportsUdfps() // initialize properties
+
+            runCurrent()
+            advanceUntilIdle()
+            verify(keyguardBlueprintRepository, times(2)).refreshBlueprint(any())
+        }
+    }
+
+    @Test
     fun testRefreshFromConfigChange() {
         testScope.runTest {
-            assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull()
+            underTest.start()
+            reset(keyguardBlueprintRepository)
 
             configurationRepository.onConfigurationChange()
 
             runCurrent()
             advanceUntilIdle()
-            assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull()
+            verify(keyguardBlueprintRepository, times(2)).refreshBlueprint(any())
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index 35659c1..14d9548 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -281,6 +281,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
index ef3183a8..ced3526 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
@@ -281,6 +281,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
index 616aac7..344e0fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
 import com.android.systemui.util.mockito.whenever
 import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -55,6 +56,7 @@
 
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
+@ExperimentalCoroutinesApi
 @SmallTest
 class DefaultKeyguardBlueprintTest : SysuiTestCase() {
     private lateinit var underTest: DefaultKeyguardBlueprint
@@ -112,17 +114,66 @@
     @Test
     fun replaceViews_withPrevBlueprint() {
         val prevBlueprint = mock(KeyguardBlueprint::class.java)
-        val someSection = mock(KeyguardSection::class.java)
-        whenever(prevBlueprint.sections)
-            .thenReturn(underTest.sections.minus(mDefaultDeviceEntrySection).plus(someSection))
+        val removedSection = mock(KeyguardSection::class.java)
+        val addedSection = mDefaultDeviceEntrySection
+        val rebuildSection = clockSection
+        val prevSections = underTest.sections.minus(addedSection).plus(removedSection)
+        val unchangedSections = underTest.sections.subtract(listOf(addedSection, rebuildSection))
+        whenever(prevBlueprint.sections).thenReturn(prevSections)
+
         val constraintLayout = ConstraintLayout(context, null)
         underTest.replaceViews(constraintLayout, prevBlueprint)
-        underTest.sections.minus(mDefaultDeviceEntrySection).forEach {
-            verify(it, never())?.addViews(constraintLayout)
+
+        unchangedSections.forEach {
+            verify(it, never()).addViews(constraintLayout)
+            verify(it, never()).removeViews(constraintLayout)
         }
 
-        verify(mDefaultDeviceEntrySection).addViews(constraintLayout)
-        verify(someSection).removeViews(constraintLayout)
+        verify(addedSection).addViews(constraintLayout)
+        verify(removedSection).removeViews(constraintLayout)
+    }
+
+    @Test
+    fun replaceViews_withPrevBlueprint_withRebuildTargets() {
+        val prevBlueprint = mock(KeyguardBlueprint::class.java)
+        val removedSection = mock(KeyguardSection::class.java)
+        val addedSection = mDefaultDeviceEntrySection
+        val rebuildSection = clockSection
+        val prevSections = underTest.sections.minus(addedSection).plus(removedSection)
+        val unchangedSections = underTest.sections.subtract(listOf(addedSection, rebuildSection))
+        whenever(prevBlueprint.sections).thenReturn(prevSections)
+
+        val constraintLayout = ConstraintLayout(context, null)
+        underTest.replaceViews(constraintLayout, prevBlueprint, listOf(rebuildSection))
+
+        unchangedSections.forEach {
+            verify(it, never()).addViews(constraintLayout)
+            verify(it, never()).removeViews(constraintLayout)
+        }
+
+        verify(addedSection).addViews(constraintLayout)
+        verify(rebuildSection).addViews(constraintLayout)
+        verify(rebuildSection).removeViews(constraintLayout)
+        verify(removedSection).removeViews(constraintLayout)
+    }
+
+    @Test
+    fun rebuildViews() {
+        val rebuildSections = listOf(mDefaultDeviceEntrySection, clockSection)
+        val unchangedSections = underTest.sections.subtract(rebuildSections)
+
+        val constraintLayout = ConstraintLayout(context, null)
+        underTest.rebuildViews(constraintLayout, rebuildSections)
+
+        unchangedSections.forEach {
+            verify(it, never()).addViews(constraintLayout)
+            verify(it, never()).removeViews(constraintLayout)
+        }
+
+        rebuildSections.forEach {
+            verify(it).addViews(constraintLayout)
+            verify(it).removeViews(constraintLayout)
+        }
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 16421a0..bdc5fc3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -178,6 +178,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 83382207..8a12a90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -221,6 +221,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 62804ed1..cb9790b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -138,6 +138,7 @@
 import com.android.systemui.settings.brightness.BrightnessSliderController;
 import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor;
 import com.android.systemui.shade.CameraLauncher;
+import com.android.systemui.shade.GlanceableHubContainerController;
 import com.android.systemui.shade.NotificationPanelView;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -337,6 +338,7 @@
     @Mock private KeyboardShortcuts mKeyboardShortcuts;
     @Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
     @Mock private PackageManager mPackageManager;
+    @Mock private GlanceableHubContainerController mGlanceableHubContainerController;
 
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
@@ -590,7 +592,8 @@
                 mUserTracker,
                 () -> mFingerprintManager,
                 mActivityStarter,
-                mBrightnessMirrorShowingInteractor
+                mBrightnessMirrorShowingInteractor,
+                mGlanceableHubContainerController
         );
         mScreenLifecycle.addObserver(mCentralSurfaces.mScreenObserver);
         mCentralSurfaces.initShadeVisibilityListener();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
index a6b40df..fb12897 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
@@ -23,12 +23,14 @@
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
+import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
 import com.android.systemui.keyguard.ui.viewmodel.keyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardSmartspaceViewModel
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.util.mockito.mock
 import java.util.Optional
+import org.mockito.Mockito.spy
 
 val Kosmos.keyguardClockSection: ClockSection by
     Kosmos.Fixture {
@@ -42,6 +44,9 @@
         )
     }
 
+val Kosmos.keyguardSmartspaceSection: SmartspaceSection by
+    Kosmos.Fixture { mock<SmartspaceSection>() }
+
 val Kosmos.defaultKeyguardBlueprint by
     Kosmos.Fixture {
         DefaultKeyguardBlueprint(
@@ -57,7 +62,7 @@
             aodBurnInSection = mock(),
             communalTutorialIndicatorSection = mock(),
             clockSection = keyguardClockSection,
-            smartspaceSection = mock(),
+            smartspaceSection = keyguardSmartspaceSection,
             keyguardSliceViewSection = mock(),
             udfpsAccessibilityOverlaySection = mock(),
             accessibilityActionsSection = mock(),
@@ -80,7 +85,7 @@
             aodBurnInSection = mock(),
             communalTutorialIndicatorSection = mock(),
             clockSection = keyguardClockSection,
-            smartspaceSection = mock(),
+            smartspaceSection = keyguardSmartspaceSection,
             mediaSection = mock(),
             accessibilityActionsSection = mock(),
         )
@@ -88,13 +93,15 @@
 
 val Kosmos.keyguardBlueprintRepository by
     Kosmos.Fixture {
-        KeyguardBlueprintRepository(
-            blueprints =
-                setOf(
-                    defaultKeyguardBlueprint,
-                    splitShadeBlueprint,
-                ),
-            handler = fakeExecutorHandler,
-            assert = mock(),
+        spy(
+            KeyguardBlueprintRepository(
+                blueprints =
+                    setOf(
+                        defaultKeyguardBlueprint,
+                        splitShadeBlueprint,
+                    ),
+                handler = fakeExecutorHandler,
+                assert = mock(),
+            )
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
index 5256ce4..4328ca1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
@@ -20,6 +20,8 @@
 import com.android.systemui.biometrics.domain.interactor.fingerprintPropertyInteractor
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
+import com.android.systemui.keyguard.data.repository.keyguardClockSection
+import com.android.systemui.keyguard.data.repository.keyguardSmartspaceSection
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -34,5 +36,7 @@
             clockInteractor = keyguardClockInteractor,
             configurationInteractor = configurationInteractor,
             fingerprintPropertyInteractor = fingerprintPropertyInteractor,
+            clockSection = keyguardClockSection,
+            smartspaceSection = keyguardSmartspaceSection,
         )
     }
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index dc1cfb9..ddccb37 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -4008,20 +4008,10 @@
     }
 
     private PackageInfo getPackageInfoForBMMLogging(String packageName) {
-        try {
-            return mPackageManager.getPackageInfoAsUser(packageName, 0, mUserId);
-        } catch (NameNotFoundException e) {
-            Slog.w(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId, "Asked to get PackageInfo for BMM logging of nonexistent pkg "
-                                    + packageName));
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
 
-            PackageInfo packageInfo = new PackageInfo();
-            packageInfo.packageName = packageName;
-
-            return packageInfo;
-        }
+        return packageInfo;
     }
 
     /** Hand off a restore session. */